feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,53 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Chain together several iterables.
*
* @deprecated
*
* @param objects - The iterable objects of interest.
*
* @returns An iterator which yields the values of the iterables
* in the order in which they are supplied.
*
* #### Example
* ```typescript
* import { chain } from '../algorithm';
*
* let data1 = [1, 2, 3];
* let data2 = [4, 5, 6];
*
* let stream = chain(data1, data2);
*
* Array.from(stream); // [1, 2, 3, 4, 5, 6]
* ```
*/
export function* chain<T>(...objects: Iterable<T>[]): IterableIterator<T> {
for (const object of objects) {
yield* object;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Create an empty iterator.
*
* @returns A new iterator which yields nothing.
*
* #### Example
* ```typescript
* import { empty } from '../algorithm';
*
* let stream = empty<number>();
*
* Array.from(stream); // []
* ```
*/
export function* empty<T>(): IterableIterator<T> {
return;
}

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Enumerate an iterable object.
*
* @param object - The iterable object of interest.
*
* @param start - The starting enum value. The default is `0`.
*
* @returns An iterator which yields the enumerated values.
*
* #### Example
* ```typescript
* import { enumerate } from '../algorithm';
*
* let data = ['foo', 'bar', 'baz'];
*
* let stream = enumerate(data, 1);
*
* Array.from(stream); // [[1, 'foo'], [2, 'bar'], [3, 'baz']]
* ```
*/
export function* enumerate<T>(
object: Iterable<T>,
start = 0,
): IterableIterator<[number, T]> {
for (const value of object) {
yield [start++, value];
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Filter an iterable for values which pass a test.
*
* @param object - The iterable object of interest.
*
* @param fn - The predicate function to invoke for each value.
*
* @returns An iterator which yields the values which pass the test.
*
* #### Example
* ```typescript
* import { filter } from '../algorithm';
*
* let data = [1, 2, 3, 4, 5, 6];
*
* let stream = filter(data, value => value % 2 === 0);
*
* Array.from(stream); // [2, 4, 6]
* ```
*/
export function* filter<T>(
object: Iterable<T>,
fn: (value: T, index: number) => boolean,
): IterableIterator<T> {
let index = 0;
for (const value of object) {
if (fn(value, index++)) {
yield value;
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Find the first value in an iterable which matches a predicate.
*
* @param object - The iterable object to search.
*
* @param fn - The predicate function to apply to the values.
*
* @returns The first matching value, or `undefined` if no matching
* value is found.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { find } from '../algorithm';
*
* interface IAnimal { species: string, name: string };
*
* function isCat(value: IAnimal): boolean {
* return value.species === 'cat';
* }
*
* let data: IAnimal[] = [
* { species: 'dog', name: 'spot' },
* { species: 'cat', name: 'fluffy' },
* { species: 'alligator', name: 'pocho' }
* ];
*
* find(data, isCat).name; // 'fluffy'
* ```
*/
export function find<T>(
object: Iterable<T>,
fn: (value: T, index: number) => boolean,
): T | undefined {
let index = 0;
for (const value of object) {
if (fn(value, index++)) {
return value;
}
}
return undefined;
}
/**
* Find the index of the first value which matches a predicate.
*
* @param object - The iterable object to search.
*
* @param fn - The predicate function to apply to the values.
*
* @returns The index of the first matching value, or `-1` if no
* matching value is found.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { findIndex } from '../algorithm';
*
* interface IAnimal { species: string, name: string };
*
* function isCat(value: IAnimal): boolean {
* return value.species === 'cat';
* }
*
* let data: IAnimal[] = [
* { species: 'dog', name: 'spot' },
* { species: 'cat', name: 'fluffy' },
* { species: 'alligator', name: 'pocho' }
* ];
*
* findIndex(data, isCat); // 1
* ```
*/
export function findIndex<T>(
object: Iterable<T>,
fn: (value: T, index: number) => boolean,
): number {
let index = 0;
for (const value of object) {
if (fn(value, index++)) {
return index - 1;
}
}
return -1;
}
/**
* Find the minimum value in an iterable.
*
* @param object - The iterable object to search.
*
* @param fn - The 3-way comparison function to apply to the values.
* It should return `< 0` if the first value is less than the second.
* `0` if the values are equivalent, or `> 0` if the first value is
* greater than the second.
*
* @returns The minimum value in the iterable. If multiple values are
* equivalent to the minimum, the left-most value is returned. If
* the iterable is empty, this returns `undefined`.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { min } from '../algorithm';
*
* function numberCmp(a: number, b: number): number {
* return a - b;
* }
*
* min([7, 4, 0, 3, 9, 4], numberCmp); // 0
* ```
*/
export function min<T>(
object: Iterable<T>,
fn: (first: T, second: T) => number,
): T | undefined {
let result: T | undefined = undefined;
for (const value of object) {
if (result === undefined) {
result = value;
continue;
}
if (fn(value, result) < 0) {
result = value;
}
}
return result;
}
/**
* Find the maximum value in an iterable.
*
* @param object - The iterable object to search.
*
* @param fn - The 3-way comparison function to apply to the values.
* It should return `< 0` if the first value is less than the second.
* `0` if the values are equivalent, or `> 0` if the first value is
* greater than the second.
*
* @returns The maximum value in the iterable. If multiple values are
* equivalent to the maximum, the left-most value is returned. If
* the iterable is empty, this returns `undefined`.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { max } from '../algorithm';
*
* function numberCmp(a: number, b: number): number {
* return a - b;
* }
*
* max([7, 4, 0, 3, 9, 4], numberCmp); // 9
* ```
*/
export function max<T>(
object: Iterable<T>,
fn: (first: T, second: T) => number,
): T | undefined {
let result: T | undefined = undefined;
for (const value of object) {
if (result === undefined) {
result = value;
continue;
}
if (fn(value, result) > 0) {
result = value;
}
}
return result;
}
/**
* Find the minimum and maximum values in an iterable.
*
* @param object - The iterable object to search.
*
* @param fn - The 3-way comparison function to apply to the values.
* It should return `< 0` if the first value is less than the second.
* `0` if the values are equivalent, or `> 0` if the first value is
* greater than the second.
*
* @returns A 2-tuple of the `[min, max]` values in the iterable. If
* multiple values are equivalent, the left-most values are returned.
* If the iterable is empty, this returns `undefined`.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { minmax } from '../algorithm';
*
* function numberCmp(a: number, b: number): number {
* return a - b;
* }
*
* minmax([7, 4, 0, 3, 9, 4], numberCmp); // [0, 9]
* ```
*/
export function minmax<T>(
object: Iterable<T>,
fn: (first: T, second: T) => number,
): [T, T] | undefined {
let empty = true;
let vmin: T;
let vmax: T;
for (const value of object) {
if (empty) {
vmin = value;
vmax = value;
empty = false;
} else if (fn(value, vmin!) < 0) {
vmin = value;
} else if (fn(value, vmax!) > 0) {
vmax = value;
}
}
return empty ? undefined : [vmin!, vmax!];
}

View File

@@ -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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module algorithm
*/
export * from './array';
export * from './chain';
export * from './empty';
export * from './enumerate';
export * from './filter';
export * from './find';
export * from './iter';
export * from './map';
export * from './range';
export * from './reduce';
export * from './repeat';
export * from './retro';
export * from './sort';
export * from './stride';
export * from './string';
export * from './take';
export * from './zip';

View File

@@ -0,0 +1,186 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Create an array from an iterable of values.
*
* @deprecated
*
* @param object - The iterable object of interest.
*
* @returns A new array of values from the given object.
*
* #### Example
* ```typescript
* import { toArray } from '../algorithm';
*
* let stream = [1, 2, 3, 4, 5, 6][Symbol.iterator]();
*
* toArray(stream); // [1, 2, 3, 4, 5, 6];
* ```
*/
export function toArray<T>(object: Iterable<T>): T[] {
return Array.from(object);
}
/**
* Create an object from an iterable of key/value pairs.
*
* @param object - The iterable object of interest.
*
* @returns A new object mapping keys to values.
*
* #### Example
* ```typescript
* import { toObject } from '../algorithm';
*
* let data: [string, number][] = [['one', 1], ['two', 2], ['three', 3]];
*
* toObject(data); // { one: 1, two: 2, three: 3 }
* ```
*/
export function toObject<T>(object: Iterable<[string, T]>): {
[key: string]: T;
} {
const result: { [key: string]: T } = {};
for (const [key, value] of object) {
result[key] = value;
}
return result;
}
/**
* Invoke a function for each value in an iterable.
*
* @deprecated
*
* @param object - The iterable object of interest.
*
* @param fn - The callback function to invoke for each value.
*
* #### Notes
* Iteration can be terminated early by returning `false` from the
* callback function.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { each } from '../algorithm';
*
* let data = [5, 7, 0, -2, 9];
*
* each(data, value => { console.log(value); });
* ```
*/
export function each<T>(
object: Iterable<T>,
fn: (value: T, index: number) => boolean | void,
): void {
let index = 0;
for (const value of object) {
if (false === fn(value, index++)) {
return;
}
}
}
/**
* Test whether all values in an iterable satisfy a predicate.
*
* @param object - The iterable object of interest.
*
* @param fn - The predicate function to invoke for each value.
*
* @returns `true` if all values pass the test, `false` otherwise.
*
* #### Notes
* Iteration terminates on the first `false` predicate result.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { every } from '../algorithm';
*
* let data = [5, 7, 1];
*
* every(data, value => value % 2 === 0); // false
* every(data, value => value % 2 === 1); // true
* ```
*/
export function every<T>(
object: Iterable<T>,
fn: (value: T, index: number) => boolean,
): boolean {
let index = 0;
for (const value of object) {
if (false === fn(value, index++)) {
return false;
}
}
return true;
}
/**
* Test whether any value in an iterable satisfies a predicate.
*
* @param object - The iterable object of interest.
*
* @param fn - The predicate function to invoke for each value.
*
* @returns `true` if any value passes the test, `false` otherwise.
*
* #### Notes
* Iteration terminates on the first `true` predicate result.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { some } from '../algorithm';
*
* let data = [5, 7, 1];
*
* some(data, value => value === 7); // true
* some(data, value => value === 3); // false
* ```
*/
export function some<T>(
object: Iterable<T>,
fn: (value: T, index: number) => boolean,
): boolean {
let index = 0;
for (const value of object) {
if (fn(value, index++)) {
return true;
}
}
return false;
}

View File

@@ -0,0 +1,54 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Transform the values of an iterable with a mapping function.
*
* @param object - The iterable object of interest.
*
* @param fn - The mapping function to invoke for each value.
*
* @returns An iterator which yields the transformed values.
*
* #### Example
* ```typescript
* import { map } from '../algorithm';
*
* let data = [1, 2, 3];
*
* let stream = map(data, value => value * 2);
*
* Array.from(stream); // [2, 4, 6]
* ```
*/
export function* map<T, U>(
object: Iterable<T>,
fn: (value: T, index: number) => U,
): IterableIterator<U> {
let index = 0;
for (const value of object) {
yield fn(value, index++);
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Create an iterator of evenly spaced values.
*
* @param start - The starting value for the range, inclusive.
*
* @param stop - The stopping value for the range, exclusive.
*
* @param step - The distance between each value.
*
* @returns An iterator which produces evenly spaced values.
*
* #### Notes
* In the single argument form of `range(stop)`, `start` defaults to
* `0` and `step` defaults to `1`.
*
* In the two argument form of `range(start, stop)`, `step` defaults
* to `1`.
*
* #### Example
* ```typescript
* import { range } from '../algorithm';
*
* let stream = range(2, 4);
*
* Array.from(stream); // [2, 3]
* ```
*/
export function* range(
start: number,
stop?: number,
step?: number,
): IterableIterator<number> {
if (stop === undefined) {
stop = start;
start = 0;
step = 1;
} else if (step === undefined) {
step = 1;
}
const length = Private.rangeLength(start, stop, step);
for (let index = 0; index < length; index++) {
yield start + step * index;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* Compute the effective length of a range.
*
* @param start - The starting value for the range, inclusive.
*
* @param stop - The stopping value for the range, exclusive.
*
* @param step - The distance between each value.
*
* @returns The number of steps need to traverse the range.
*/
export function rangeLength(
start: number,
stop: number,
step: number,
): number {
if (step === 0) {
return Infinity;
}
if (start > stop && step > 0) {
return 0;
}
if (start < stop && step < 0) {
return 0;
}
return Math.ceil((stop - start) / step);
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Summarize all values in an iterable using a reducer function.
*
* @param object - The iterable object of interest.
*
* @param fn - The reducer function to invoke for each value.
*
* @param initial - The initial value to start accumulation.
*
* @returns The final accumulated value.
*
* #### Notes
* The `reduce` function follows the conventions of `Array#reduce`.
*
* If the iterator is empty, an initial value is required. That value
* will be used as the return value. If no initial value is provided,
* an error will be thrown.
*
* If the iterator contains a single item and no initial value is
* provided, the single item is used as the return value.
*
* Otherwise, the reducer is invoked for each element in the iterable.
* If an initial value is not provided, the first element will be used
* as the initial accumulated value.
*
* #### Complexity
* Linear.
*
* #### Example
* ```typescript
* import { reduce } from '../algorithm';
*
* let data = [1, 2, 3, 4, 5];
*
* let sum = reduce(data, (a, value) => a + value); // 15
* ```
*/
export function reduce<T>(
object: Iterable<T>,
fn: (accumulator: T, value: T, index: number) => T,
): T;
export function reduce<T, U>(
object: Iterable<T>,
fn: (accumulator: U, value: T, index: number) => U,
initial: U,
): U;
export function reduce<T>(
object: Iterable<T>,
fn: (accumulator: any, value: T, index: number) => any,
initial?: unknown,
): any {
// Setup the iterator and fetch the first value.
const it = object[Symbol.iterator]();
let index = 0;
const first = it.next();
// An empty iterator and no initial value is an error.
if (first.done && initial === undefined) {
throw new TypeError('Reduce of empty iterable with no initial value.');
}
// If the iterator is empty, return the initial value.
if (first.done) {
return initial;
}
// If the iterator has a single item and no initial value, the
// reducer is not invoked and the first item is the return value.
const second = it.next();
if (second.done && initial === undefined) {
return first.value;
}
// If iterator has a single item and an initial value is provided,
// the reducer is invoked and that result is the return value.
if (second.done) {
return fn(initial, first.value, index++);
}
// Setup the initial accumlated value.
let accumulator: any;
if (initial === undefined) {
accumulator = fn(first.value, second.value, index++);
} else {
accumulator = fn(fn(initial, first.value, index++), second.value, index++);
}
// Iterate the rest of the values, updating the accumulator.
let next: IteratorResult<T>;
while (!(next = it.next()).done) {
accumulator = fn(accumulator, next.value, index++);
}
// Return the final accumulated value.
return accumulator;
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Create an iterator which repeats a value a number of times.
*
* @deprecated
*
* @param value - The value to repeat.
*
* @param count - The number of times to repeat the value.
*
* @returns A new iterator which repeats the specified value.
*
* #### Example
* ```typescript
* import { repeat } from '../algorithm';
*
* let stream = repeat(7, 3);
*
* Array.from(stream); // [7, 7, 7]
* ```
*/
export function* repeat<T>(value: T, count: number): IterableIterator<T> {
while (0 < count--) {
yield value;
}
}
/**
* Create an iterator which yields a value a single time.
*
* @deprecated
*
* @param value - The value to wrap in an iterator.
*
* @returns A new iterator which yields the value a single time.
*
* #### Example
* ```typescript
* import { once } from '../algorithm';
*
* let stream = once(7);
*
* Array.from(stream); // [7]
* ```
*/
export function* once<T>(value: T): IterableIterator<T> {
yield value;
}

View File

@@ -0,0 +1,67 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* An object which can produce a reverse iterator over its values.
*/
export interface IRetroable<T> {
/**
* Get a reverse iterator over the object's values.
*
* @returns An iterator which yields the object's values in reverse.
*/
retro(): IterableIterator<T>;
}
/**
* Create an iterator for a retroable object.
*
* @param object - The retroable or array-like object of interest.
*
* @returns An iterator which traverses the object's values in reverse.
*
* #### Example
* ```typescript
* import { retro } from '../algorithm';
*
* let data = [1, 2, 3, 4, 5, 6];
*
* let stream = retro(data);
*
* Array.from(stream); // [6, 5, 4, 3, 2, 1]
* ```
*/
export function* retro<T>(
object: IRetroable<T> | ArrayLike<T>,
): IterableIterator<T> {
if (typeof (object as IRetroable<T>).retro === 'function') {
yield* (object as IRetroable<T>).retro();
} else {
for (let index = (object as ArrayLike<T>).length - 1; index > -1; index--) {
yield (object as ArrayLike<T>)[index];
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Topologically sort an iterable of edges.
*
* @param edges - The iterable object of edges to sort.
* An edge is represented as a 2-tuple of `[fromNode, toNode]`.
*
* @returns The topologically sorted array of nodes.
*
* #### Notes
* If a cycle is present in the graph, the cycle will be ignored and
* the return value will be only approximately sorted.
*
* #### Example
* ```typescript
* import { topologicSort } from '../algorithm';
*
* let data = [
* ['d', 'e'],
* ['c', 'd'],
* ['a', 'b'],
* ['b', 'c']
* ];
*
* topologicSort(data); // ['a', 'b', 'c', 'd', 'e']
* ```
*/
export function topologicSort<T>(edges: Iterable<[T, T]>): T[] {
// Setup the shared sorting state.
const sorted: T[] = [];
const visited = new Set<T>();
const graph = new Map<T, T[]>();
// Add the edges to the graph.
for (const edge of edges) {
addEdge(edge);
}
// Visit each node in the graph.
for (const [k] of graph) {
visit(k);
}
// Return the sorted results.
return sorted;
// Add an edge to the graph.
function addEdge(edge: [T, T]): void {
const [fromNode, toNode] = edge;
const children = graph.get(toNode);
if (children) {
children.push(fromNode);
} else {
graph.set(toNode, [fromNode]);
}
}
// Recursively visit the node.
function visit(node: T): void {
if (visited.has(node)) {
return;
}
visited.add(node);
const children = graph.get(node);
if (children) {
for (const child of children) {
visit(child);
}
}
sorted.push(node);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Iterate over an iterable using a stepped increment.
*
* @param object - The iterable object of interest.
*
* @param step - The distance to step on each iteration. A value
* of less than `1` will behave the same as a value of `1`.
*
* @returns An iterator which traverses the iterable step-wise.
*
* #### Example
* ```typescript
* import { stride } from '../algorithm';
*
* let data = [1, 2, 3, 4, 5, 6];
*
* let stream = stride(data, 2);
*
* Array.from(stream); // [1, 3, 5];
* ```
*/
export function* stride<T>(
object: Iterable<T>,
step: number,
): IterableIterator<T> {
let count = 0;
for (const value of object) {
if (0 === count++ % step) {
yield value;
}
}
}

View File

@@ -0,0 +1,240 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* The namespace for string-specific algorithms.
*/
export namespace StringExt {
/**
* Find the indices of characters in a source text.
*
* @param source - The source text which should be searched.
*
* @param query - The characters to locate in the source text.
*
* @param start - The index to start the search.
*
* @returns The matched indices, or `null` if there is no match.
*
* #### Complexity
* Linear on `sourceText`.
*
* #### Notes
* In order for there to be a match, all of the characters in `query`
* **must** appear in `source` in the order given by `query`.
*
* Characters are matched using strict `===` equality.
*/
export function findIndices(
source: string,
query: string,
start = 0,
): number[] | null {
const indices = new Array<number>(query.length);
for (let i = 0, j = start, n = query.length; i < n; ++i, ++j) {
j = source.indexOf(query[i], j);
if (j === -1) {
return null;
}
indices[i] = j;
}
return indices;
}
/**
* The result of a string match function.
*/
export interface IMatchResult {
/**
* A score which indicates the strength of the match.
*
* The documentation of a given match function should specify
* whether a lower or higher score is a stronger match.
*/
score: number;
/**
* The indices of the matched characters in the source text.
*
* The indices will appear in increasing order.
*/
indices: number[];
}
/**
* A string matcher which uses a sum-of-squares algorithm.
*
* @param source - The source text which should be searched.
*
* @param query - The characters to locate in the source text.
*
* @param start - The index to start the search.
*
* @returns The match result, or `null` if there is no match.
* A lower `score` represents a stronger match.
*
* #### Complexity
* Linear on `sourceText`.
*
* #### Notes
* This scoring algorithm uses a sum-of-squares approach to determine
* the score. In order for there to be a match, all of the characters
* in `query` **must** appear in `source` in order. The index of each
* matching character is squared and added to the score. This means
* that early and consecutive character matches are preferred, while
* late matches are heavily penalized.
*/
export function matchSumOfSquares(
source: string,
query: string,
start = 0,
): IMatchResult | null {
const indices = findIndices(source, query, start);
if (!indices) {
return null;
}
let score = 0;
for (let i = 0, n = indices.length; i < n; ++i) {
const j = indices[i] - start;
score += j * j;
}
return { score, indices };
}
/**
* A string matcher which uses a sum-of-deltas algorithm.
*
* @param source - The source text which should be searched.
*
* @param query - The characters to locate in the source text.
*
* @param start - The index to start the search.
*
* @returns The match result, or `null` if there is no match.
* A lower `score` represents a stronger match.
*
* #### Complexity
* Linear on `sourceText`.
*
* #### Notes
* This scoring algorithm uses a sum-of-deltas approach to determine
* the score. In order for there to be a match, all of the characters
* in `query` **must** appear in `source` in order. The delta between
* the indices are summed to create the score. This means that groups
* of matched characters are preferred, while fragmented matches are
* penalized.
*/
export function matchSumOfDeltas(
source: string,
query: string,
start = 0,
): IMatchResult | null {
const indices = findIndices(source, query, start);
if (!indices) {
return null;
}
let score = 0;
let last = start - 1;
for (let i = 0, n = indices.length; i < n; ++i) {
const j = indices[i];
score += j - last - 1;
last = j;
}
return { score, indices };
}
/**
* Highlight the matched characters of a source text.
*
* @param source - The text which should be highlighted.
*
* @param indices - The indices of the matched characters. They must
* appear in increasing order and must be in bounds of the source.
*
* @param fn - The function to apply to the matched chunks.
*
* @returns An array of unmatched and highlighted chunks.
*/
export function highlight<T>(
source: string,
indices: ReadonlyArray<number>,
fn: (chunk: string) => T,
): Array<string | T> {
// Set up the result array.
const result: Array<string | T> = [];
// Set up the counter variables.
let k = 0;
let last = 0;
const n = indices.length;
// Iterator over each index.
while (k < n) {
// Set up the chunk indices.
const i = indices[k];
let j = indices[k];
// Advance the right chunk index until it's non-contiguous.
while (++k < n && indices[k] === j + 1) {
j++;
}
// Extract the unmatched text.
if (last < i) {
result.push(source.slice(last, i));
}
// Extract and highlight the matched text.
if (i < j + 1) {
result.push(fn(source.slice(i, j + 1)));
}
// Update the last visited index.
last = j + 1;
}
// Extract any remaining unmatched text.
if (last < source.length) {
result.push(source.slice(last));
}
// Return the highlighted result.
return result;
}
/**
* A 3-way string comparison function.
*
* @param a - The first string of interest.
*
* @param b - The second string of interest.
*
* @returns `-1` if `a < b`, else `1` if `a > b`, else `0`.
*/
export function cmp(a: string, b: string): number {
return a < b ? -1 : a > b ? 1 : 0;
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* Take a fixed number of items from an iterable.
*
* @param object - The iterable object of interest.
*
* @param count - The number of items to take from the iterable.
*
* @returns An iterator which yields the specified number of items
* from the source iterable.
*
* #### Notes
* The returned iterator will exhaust early if the source iterable
* contains an insufficient number of items.
*
* #### Example
* ```typescript
* import { take } from '../algorithm';
*
* let stream = take([5, 4, 3, 2, 1, 0, -1], 3);
*
* Array.from(stream); // [5, 4, 3]
* ```
*/
export function* take<T>(
object: Iterable<T>,
count: number,
): IterableIterator<T> {
if (count < 1) {
return;
}
const it = object[Symbol.iterator]();
let item: IteratorResult<T>;
while (0 < count-- && !(item = it.next()).done) {
yield item.value;
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { every } from './iter';
/**
* Iterate several iterables in lockstep.
*
* @param objects - The iterable objects of interest.
*
* @returns An iterator which yields successive tuples of values where
* each value is taken in turn from the provided iterables. It will
* be as long as the shortest provided iterable.
*
* #### Example
* ```typescript
* import { zip } from '../algorithm';
*
* let data1 = [1, 2, 3];
* let data2 = [4, 5, 6];
*
* let stream = zip(data1, data2);
*
* Array.from(stream); // [[1, 4], [2, 5], [3, 6]]
* ```
*/
export function* zip<T>(...objects: Iterable<T>[]): IterableIterator<T[]> {
const iters = objects.map(obj => obj[Symbol.iterator]());
let tuple = iters.map(it => it.next());
for (; every(tuple, item => !item.done); tuple = iters.map(it => it.next())) {
yield tuple.map(item => item.value);
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module collections
*/
export * from './linkedlist';

View File

@@ -0,0 +1,587 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type IRetroable } from '../algorithm';
/**
* A generic doubly-linked list.
*/
export class LinkedList<T> implements Iterable<T>, IRetroable<T> {
/**
* Whether the list is empty.
*
* #### Complexity
* Constant.
*/
get isEmpty(): boolean {
return this._size === 0;
}
/**
* The size of the list.
*
* #### Complexity
* `O(1)`
*
* #### Notes
* This is equivalent to `length`.
*/
get size(): number {
return this._size;
}
/**
* The length of the list.
*
* #### Complexity
* Constant.
*
* #### Notes
* This is equivalent to `size`.
*
* This property is deprecated.
*/
get length(): number {
return this._size;
}
/**
* The first value in the list.
*
* This is `undefined` if the list is empty.
*
* #### Complexity
* Constant.
*/
get first(): T | undefined {
return this._first ? this._first.value : undefined;
}
/**
* The last value in the list.
*
* This is `undefined` if the list is empty.
*
* #### Complexity
* Constant.
*/
get last(): T | undefined {
return this._last ? this._last.value : undefined;
}
/**
* The first node in the list.
*
* This is `null` if the list is empty.
*
* #### Complexity
* Constant.
*/
get firstNode(): LinkedList.INode<T> | null {
return this._first;
}
/**
* The last node in the list.
*
* This is `null` if the list is empty.
*
* #### Complexity
* Constant.
*/
get lastNode(): LinkedList.INode<T> | null {
return this._last;
}
/**
* Create an iterator over the values in the list.
*
* @returns A new iterator starting with the first value.
*
* #### Complexity
* Constant.
*/
*[Symbol.iterator](): IterableIterator<T> {
let node = this._first;
while (node) {
yield node.value;
node = node.next;
}
}
/**
* Create a reverse iterator over the values in the list.
*
* @returns A new iterator starting with the last value.
*
* #### Complexity
* Constant.
*/
*retro(): IterableIterator<T> {
let node = this._last;
while (node) {
yield node.value;
node = node.prev;
}
}
/**
* Create an iterator over the nodes in the list.
*
* @returns A new iterator starting with the first node.
*
* #### Complexity
* Constant.
*/
*nodes(): IterableIterator<LinkedList.INode<T>> {
let node = this._first;
while (node) {
yield node;
node = node.next;
}
}
/**
* Create a reverse iterator over the nodes in the list.
*
* @returns A new iterator starting with the last node.
*
* #### Complexity
* Constant.
*/
*retroNodes(): IterableIterator<LinkedList.INode<T>> {
let node = this._last;
while (node) {
yield node;
node = node.prev;
}
}
/**
* Assign new values to the list, replacing all current values.
*
* @param values - The values to assign to the list.
*
* #### Complexity
* Linear.
*/
assign(values: Iterable<T>): void {
this.clear();
for (const value of values) {
this.addLast(value);
}
}
/**
* Add a value to the end of the list.
*
* @param value - The value to add to the end of the list.
*
* #### Complexity
* Constant.
*
* #### Notes
* This is equivalent to `addLast`.
*/
push(value: T): void {
this.addLast(value);
}
/**
* Remove and return the value at the end of the list.
*
* @returns The removed value, or `undefined` if the list is empty.
*
* #### Complexity
* Constant.
*
* #### Notes
* This is equivalent to `removeLast`.
*/
pop(): T | undefined {
return this.removeLast();
}
/**
* Add a value to the beginning of the list.
*
* @param value - The value to add to the beginning of the list.
*
* #### Complexity
* Constant.
*
* #### Notes
* This is equivalent to `addFirst`.
*/
shift(value: T): void {
this.addFirst(value);
}
/**
* Remove and return the value at the beginning of the list.
*
* @returns The removed value, or `undefined` if the list is empty.
*
* #### Complexity
* Constant.
*
* #### Notes
* This is equivalent to `removeFirst`.
*/
unshift(): T | undefined {
return this.removeFirst();
}
/**
* Add a value to the beginning of the list.
*
* @param value - The value to add to the beginning of the list.
*
* @returns The list node which holds the value.
*
* #### Complexity
* Constant.
*/
addFirst(value: T): LinkedList.INode<T> {
const node = new Private.LinkedListNode<T>(this, value);
if (!this._first) {
this._first = node;
this._last = node;
} else {
node.next = this._first;
this._first.prev = node;
this._first = node;
}
this._size++;
return node;
}
/**
* Add a value to the end of the list.
*
* @param value - The value to add to the end of the list.
*
* @returns The list node which holds the value.
*
* #### Complexity
* Constant.
*/
addLast(value: T): LinkedList.INode<T> {
const node = new Private.LinkedListNode<T>(this, value);
if (!this._last) {
this._first = node;
this._last = node;
} else {
node.prev = this._last;
this._last.next = node;
this._last = node;
}
this._size++;
return node;
}
/**
* Insert a value before a specific node in the list.
*
* @param value - The value to insert before the reference node.
*
* @param ref - The reference node of interest. If this is `null`,
* the value will be added to the beginning of the list.
*
* @returns The list node which holds the value.
*
* #### Notes
* The reference node must be owned by the list.
*
* #### Complexity
* Constant.
*/
insertBefore(value: T, ref: LinkedList.INode<T> | null): LinkedList.INode<T> {
if (!ref || ref === this._first) {
return this.addFirst(value);
}
if (!(ref instanceof Private.LinkedListNode) || ref.list !== this) {
throw new Error('Reference node is not owned by the list.');
}
const node = new Private.LinkedListNode<T>(this, value);
const _ref = ref as Private.LinkedListNode<T>;
const prev = _ref.prev!;
node.next = _ref;
node.prev = prev;
_ref.prev = node;
prev.next = node;
this._size++;
return node;
}
/**
* Insert a value after a specific node in the list.
*
* @param value - The value to insert after the reference node.
*
* @param ref - The reference node of interest. If this is `null`,
* the value will be added to the end of the list.
*
* @returns The list node which holds the value.
*
* #### Notes
* The reference node must be owned by the list.
*
* #### Complexity
* Constant.
*/
insertAfter(value: T, ref: LinkedList.INode<T> | null): LinkedList.INode<T> {
if (!ref || ref === this._last) {
return this.addLast(value);
}
if (!(ref instanceof Private.LinkedListNode) || ref.list !== this) {
throw new Error('Reference node is not owned by the list.');
}
const node = new Private.LinkedListNode<T>(this, value);
const _ref = ref as Private.LinkedListNode<T>;
const next = _ref.next!;
node.next = next;
node.prev = _ref;
_ref.next = node;
next.prev = node;
this._size++;
return node;
}
/**
* Remove and return the value at the beginning of the list.
*
* @returns The removed value, or `undefined` if the list is empty.
*
* #### Complexity
* Constant.
*/
removeFirst(): T | undefined {
const node = this._first;
if (!node) {
return undefined;
}
if (node === this._last) {
this._first = null;
this._last = null;
} else {
this._first = node.next;
this._first!.prev = null;
}
node.list = null;
node.next = null;
node.prev = null;
this._size--;
return node.value;
}
/**
* Remove and return the value at the end of the list.
*
* @returns The removed value, or `undefined` if the list is empty.
*
* #### Complexity
* Constant.
*/
removeLast(): T | undefined {
const node = this._last;
if (!node) {
return undefined;
}
if (node === this._first) {
this._first = null;
this._last = null;
} else {
this._last = node.prev;
this._last!.next = null;
}
node.list = null;
node.next = null;
node.prev = null;
this._size--;
return node.value;
}
/**
* Remove a specific node from the list.
*
* @param node - The node to remove from the list.
*
* #### Complexity
* Constant.
*
* #### Notes
* The node must be owned by the list.
*/
removeNode(node: LinkedList.INode<T>): void {
if (!(node instanceof Private.LinkedListNode) || node.list !== this) {
throw new Error('Node is not owned by the list.');
}
const _node = node as Private.LinkedListNode<T>;
if (_node === this._first && _node === this._last) {
this._first = null;
this._last = null;
} else if (_node === this._first) {
this._first = _node.next;
this._first!.prev = null;
} else if (_node === this._last) {
this._last = _node.prev;
this._last!.next = null;
} else {
_node.next!.prev = _node.prev;
_node.prev!.next = _node.next;
}
_node.list = null;
_node.next = null;
_node.prev = null;
this._size--;
}
/**
* Remove all values from the list.
*
* #### Complexity
* Linear.
*/
clear(): void {
let node = this._first;
while (node) {
const { next } = node;
node.list = null;
node.prev = null;
node.next = null;
node = next;
}
this._first = null;
this._last = null;
this._size = 0;
}
private _first: Private.LinkedListNode<T> | null = null;
private _last: Private.LinkedListNode<T> | null = null;
private _size = 0;
}
/**
* The namespace for the `LinkedList` class statics.
*/
export namespace LinkedList {
/**
* An object which represents a node in a linked list.
*
* #### Notes
* User code will not create linked list nodes directly. Nodes
* are created automatically when values are added to a list.
*/
export interface INode<T> {
/**
* The linked list which created and owns the node.
*
* This will be `null` when the node is removed from the list.
*/
readonly list: LinkedList<T> | null;
/**
* The next node in the list.
*
* This will be `null` when the node is the last node in the list
* or when the node is removed from the list.
*/
readonly next: INode<T> | null;
/**
* The previous node in the list.
*
* This will be `null` when the node is the first node in the list
* or when the node is removed from the list.
*/
readonly prev: INode<T> | null;
/**
* The user value stored in the node.
*/
readonly value: T;
}
/**
* Create a linked list from an iterable of values.
*
* @param values - The iterable object of interest.
*
* @returns A new linked list initialized with the given values.
*
* #### Complexity
* Linear.
*/
export function from<T>(values: Iterable<T>): LinkedList<T> {
const list = new LinkedList<T>();
list.assign(values);
return list;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* The internal linked list node implementation.
*/
export class LinkedListNode<T> {
/**
* The linked list which created and owns the node.
*/
list: LinkedList<T> | null = null;
/**
* The next node in the list.
*/
next: LinkedListNode<T> | null = null;
/**
* The previous node in the list.
*/
prev: LinkedListNode<T> | null = null;
/**
* The user value stored in the node.
*/
readonly value: T;
/**
* Construct a new linked list node.
*
* @param list - The list which owns the node.
*
* @param value - The value for the link.
*/
constructor(list: LinkedList<T>, value: T) {
this.list = list;
this.value = value;
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
export * from './json';
export * from './mime';
export * from './promise';
export * from './token';

View File

@@ -0,0 +1,24 @@
/*
* 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.
*/
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
export * from './index.common';
export * from './random.node';
export * from './uuid.node';

View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module coreutils
*/
export * from './index.common';
export * from './random.browser';
export * from './uuid.browser';

View File

@@ -0,0 +1,365 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A type alias for a JSON primitive.
*/
export type JSONPrimitive = boolean | number | string | null;
/**
* A type alias for a JSON value.
*/
export type JSONValue = JSONPrimitive | JSONObject | JSONArray;
/**
* A type definition for a JSON object.
*/
export interface JSONObject {
[key: string]: JSONValue;
}
/**
* A type definition for a JSON array.
*/
export interface JSONArray extends Array<JSONValue> {}
/**
* A type definition for a readonly JSON object.
*/
export interface ReadonlyJSONObject {
readonly [key: string]: ReadonlyJSONValue;
}
/**
* A type definition for a readonly JSON array.
*/
export interface ReadonlyJSONArray extends ReadonlyArray<ReadonlyJSONValue> {}
/**
* A type alias for a readonly JSON value.
*/
export type ReadonlyJSONValue =
| JSONPrimitive
| ReadonlyJSONObject
| ReadonlyJSONArray;
/**
* A type alias for a partial JSON value.
*
* Note: Partial here means that JSON object attributes can be `undefined`.
*/
export type PartialJSONValue =
| JSONPrimitive
| PartialJSONObject
| PartialJSONArray;
/**
* A type definition for a partial JSON object.
*
* Note: Partial here means that the JSON object attributes can be `undefined`.
*/
export interface PartialJSONObject {
[key: string]: PartialJSONValue | undefined;
}
/**
* A type definition for a partial JSON array.
*
* Note: Partial here means that JSON object attributes can be `undefined`.
*/
export interface PartialJSONArray extends Array<PartialJSONValue> {}
/**
* A type definition for a readonly partial JSON object.
*
* Note: Partial here means that JSON object attributes can be `undefined`.
*/
export interface ReadonlyPartialJSONObject {
readonly [key: string]: ReadonlyPartialJSONValue | undefined;
}
/**
* A type definition for a readonly partial JSON array.
*
* Note: Partial here means that JSON object attributes can be `undefined`.
*/
export interface ReadonlyPartialJSONArray
extends ReadonlyArray<ReadonlyPartialJSONValue> {}
/**
* A type alias for a readonly partial JSON value.
*
* Note: Partial here means that JSON object attributes can be `undefined`.
*/
export type ReadonlyPartialJSONValue =
| JSONPrimitive
| ReadonlyPartialJSONObject
| ReadonlyPartialJSONArray;
/**
* The namespace for JSON-specific functions.
*/
export namespace JSONExt {
/**
* A shared frozen empty JSONObject
*/
export const emptyObject = Object.freeze({}) as ReadonlyJSONObject;
/**
* A shared frozen empty JSONArray
*/
export const emptyArray = Object.freeze([]) as ReadonlyJSONArray;
/**
* Test whether a JSON value is a primitive.
*
* @param value - The JSON value of interest.
*
* @returns `true` if the value is a primitive,`false` otherwise.
*/
export function isPrimitive(
value: ReadonlyPartialJSONValue,
): value is JSONPrimitive {
return (
value === null ||
typeof value === 'boolean' ||
typeof value === 'number' ||
typeof value === 'string'
);
}
/**
* Test whether a JSON value is an array.
*
* @param value - The JSON value of interest.
*
* @returns `true` if the value is a an array, `false` otherwise.
*/
export function isArray(value: JSONValue): value is JSONArray;
export function isArray(value: ReadonlyJSONValue): value is ReadonlyJSONArray;
export function isArray(value: PartialJSONValue): value is PartialJSONArray;
export function isArray(
value: ReadonlyPartialJSONValue,
): value is ReadonlyPartialJSONArray;
export function isArray(value: ReadonlyPartialJSONValue): boolean {
return Array.isArray(value);
}
/**
* Test whether a JSON value is an object.
*
* @param value - The JSON value of interest.
*
* @returns `true` if the value is a an object, `false` otherwise.
*/
export function isObject(value: JSONValue): value is JSONObject;
export function isObject(
value: ReadonlyJSONValue,
): value is ReadonlyJSONObject;
export function isObject(value: PartialJSONValue): value is PartialJSONObject;
export function isObject(
value: ReadonlyPartialJSONValue,
): value is ReadonlyPartialJSONObject;
export function isObject(value: ReadonlyPartialJSONValue): boolean {
return !isPrimitive(value) && !isArray(value);
}
/**
* Compare two JSON values for deep equality.
*
* @param first - The first JSON value of interest.
*
* @param second - The second JSON value of interest.
*
* @returns `true` if the values are equivalent, `false` otherwise.
*/
export function deepEqual(
first: ReadonlyPartialJSONValue,
second: ReadonlyPartialJSONValue,
): boolean {
// Check referential and primitive equality first.
if (first === second) {
return true;
}
// If one is a primitive, the `===` check ruled out the other.
if (isPrimitive(first) || isPrimitive(second)) {
return false;
}
// Test whether they are arrays.
const a1 = isArray(first);
const a2 = isArray(second);
// Bail if the types are different.
if (a1 !== a2) {
return false;
}
// If they are both arrays, compare them.
if (a1 && a2) {
return deepArrayEqual(
first as ReadonlyPartialJSONArray,
second as ReadonlyPartialJSONArray,
);
}
// At this point, they must both be objects.
return deepObjectEqual(
first as ReadonlyPartialJSONObject,
second as ReadonlyPartialJSONObject,
);
}
/**
* Create a deep copy of a JSON value.
*
* @param value - The JSON value to copy.
*
* @returns A deep copy of the given JSON value.
*/
export function deepCopy<T extends ReadonlyPartialJSONValue>(value: T): T {
// Do nothing for primitive values.
if (isPrimitive(value)) {
return value;
}
// Deep copy an array.
if (isArray(value)) {
return deepArrayCopy(value);
}
// Deep copy an object.
return deepObjectCopy(value);
}
/**
* Compare two JSON arrays for deep equality.
*/
function deepArrayEqual(
first: ReadonlyPartialJSONArray,
second: ReadonlyPartialJSONArray,
): boolean {
// Check referential equality first.
if (first === second) {
return true;
}
// Test the arrays for equal length.
if (first.length !== second.length) {
return false;
}
// Compare the values for equality.
for (let i = 0, n = first.length; i < n; ++i) {
if (!deepEqual(first[i], second[i])) {
return false;
}
}
// At this point, the arrays are equal.
return true;
}
/**
* Compare two JSON objects for deep equality.
*/
function deepObjectEqual(
first: ReadonlyPartialJSONObject,
second: ReadonlyPartialJSONObject,
): boolean {
// Check referential equality first.
if (first === second) {
return true;
}
// Check for the first object's keys in the second object.
for (const key in first) {
if (first[key] !== undefined && !(key in second)) {
return false;
}
}
// Check for the second object's keys in the first object.
for (const key in second) {
if (second[key] !== undefined && !(key in first)) {
return false;
}
}
// Compare the values for equality.
for (const key in first) {
// Get the values.
const firstValue = first[key];
const secondValue = second[key];
// If both are undefined, ignore the key.
if (firstValue === undefined && secondValue === undefined) {
continue;
}
// If only one value is undefined, the objects are not equal.
if (firstValue === undefined || secondValue === undefined) {
return false;
}
// Compare the values.
if (!deepEqual(firstValue, secondValue)) {
return false;
}
}
// At this point, the objects are equal.
return true;
}
/**
* Create a deep copy of a JSON array.
*/
function deepArrayCopy(value: any): any {
const result = new Array<any>(value.length);
for (let i = 0, n = value.length; i < n; ++i) {
result[i] = deepCopy(value[i]);
}
return result;
}
/**
* Create a deep copy of a JSON object.
*/
function deepObjectCopy(value: any): any {
const result: any = {};
for (const key in value) {
// Ignore undefined values.
const subvalue = value[key];
if (subvalue === undefined) {
continue;
}
result[key] = deepCopy(subvalue);
}
return result;
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* An object which stores MIME data for general application use.
*
* #### Notes
* This class does not attempt to enforce "correctness" of MIME types
* and their associated data. Since this class is designed to transfer
* arbitrary data and objects within the same application, it assumes
* that the user provides correct and accurate data.
*/
export class MimeData {
/**
* Get an array of the MIME types contained within the dataset.
*
* @returns A new array of the MIME types, in order of insertion.
*/
types(): string[] {
return this._types.slice();
}
/**
* Test whether the dataset has an entry for the given type.
*
* @param mime - The MIME type of interest.
*
* @returns `true` if the dataset contains a value for the given
* MIME type, `false` otherwise.
*/
hasData(mime: string): boolean {
return this._types.indexOf(mime) !== -1;
}
/**
* Get the data value for the given MIME type.
*
* @param mime - The MIME type of interest.
*
* @returns The value for the given MIME type, or `undefined` if
* the dataset does not contain a value for the type.
*/
getData(mime: string): any | undefined {
const i = this._types.indexOf(mime);
return i !== -1 ? this._values[i] : undefined;
}
/**
* Set the data value for the given MIME type.
*
* @param mime - The MIME type of interest.
*
* @param data - The data value for the given MIME type.
*
* #### Notes
* This will overwrite any previous entry for the MIME type.
*/
setData(mime: string, data: unknown): void {
this.clearData(mime);
this._types.push(mime);
this._values.push(data);
}
/**
* Remove the data entry for the given MIME type.
*
* @param mime - The MIME type of interest.
*
* #### Notes
* This is a no-op if there is no entry for the given MIME type.
*/
clearData(mime: string): void {
const i = this._types.indexOf(mime);
if (i !== -1) {
this._types.splice(i, 1);
this._values.splice(i, 1);
}
}
/**
* Remove all data entries from the dataset.
*/
clear(): void {
this._types.length = 0;
this._values.length = 0;
}
private _types: string[] = [];
private _values: any[] = [];
}

View File

@@ -0,0 +1,73 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A class which wraps a promise into a delegate object.
*
* #### Notes
* This class is useful when the logic to resolve or reject a promise
* cannot be defined at the point where the promise is created.
*/
export class PromiseDelegate<T> {
/**
* Construct a new promise delegate.
*/
constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}
/**
* The promise wrapped by the delegate.
*/
readonly promise: Promise<T>;
/**
* Resolve the wrapped promise with the given value.
*
* @param value - The value to use for resolving the promise.
*/
resolve(value: T | PromiseLike<T>): void {
const resolve = this._resolve;
resolve(value);
}
/**
* Reject the wrapped promise with the given value.
*
* @reason - The reason for rejecting the promise.
*/
reject(reason: unknown): void {
const reject = this._reject;
reject(reason);
}
private _resolve: (value: T | PromiseLike<T>) => void;
private _reject: (reason: any) => void;
}

View File

@@ -0,0 +1,70 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { fallbackRandomValues } from './random';
// Declare ambient variables for `window` and `require` to avoid a
// hard dependency on both. This package must run on node.
declare let window: any;
/**
* The namespace for random number related functionality.
*/
export namespace Random {
/**
* A function which generates random bytes.
*
* @param buffer - The `Uint8Array` to fill with random bytes.
*
* #### Notes
* A cryptographically strong random number generator will be used if
* available. Otherwise, `Math.random` will be used as a fallback for
* randomness.
*
* The following RNGs are supported, listed in order of precedence:
* - `window.crypto.getRandomValues`
* - `window.msCrypto.getRandomValues`
* - `require('crypto').randomFillSync
* - `require('crypto').randomBytes
* - `Math.random`
*/
export const getRandomValues = (() => {
// Look up the crypto module if available.
const crypto: any =
(typeof window !== 'undefined' && (window.crypto || window.msCrypto)) ||
null;
// Modern browsers and IE 11
if (crypto && typeof crypto.getRandomValues === 'function') {
return function getRandomValues(buffer: Uint8Array): void {
return crypto.getRandomValues(buffer);
};
}
// Fallback
return fallbackRandomValues;
})();
}

View File

@@ -0,0 +1,79 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { fallbackRandomValues } from './random';
// Declare ambient variables for `window` and `require` to avoid a
// hard dependency on both. This package must run on node.
declare let require: any;
/**
* The namespace for random number related functionality.
*/
export namespace Random {
/**
* A function which generates random bytes.
*
* @param buffer - The `Uint8Array` to fill with random bytes.
*
* #### Notes
* A cryptographically strong random number generator will be used if
* available. Otherwise, `Math.random` will be used as a fallback for
* randomness.
*
* The following RNGs are supported, listed in order of precedence:
* - `window.crypto.getRandomValues`
* - `window.msCrypto.getRandomValues`
* - `require('crypto').randomFillSync
* - `require('crypto').randomBytes
* - `Math.random`
*/
export const getRandomValues = (() => {
// Look up the crypto module if available.
const crypto: any =
(typeof require !== 'undefined' && require('crypto')) || null;
// Node 7+
if (crypto && typeof crypto.randomFillSync === 'function') {
return function getRandomValues(buffer: Uint8Array): void {
return crypto.randomFillSync(buffer);
};
}
// Node 0.10+
if (crypto && typeof crypto.randomBytes === 'function') {
return function getRandomValues(buffer: Uint8Array): void {
const bytes = crypto.randomBytes(buffer.length);
for (let i = 0, n = bytes.length; i < n; ++i) {
buffer[i] = bytes[i];
}
};
}
// Fallback
return fallbackRandomValues;
})();
}

View File

@@ -0,0 +1,37 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
// Fallback
export function fallbackRandomValues(buffer: Uint8Array): void {
let value = 0;
for (let i = 0, n = buffer.length; i < n; ++i) {
if (i % 4 === 0) {
value = (Math.random() * 0xffffffff) >>> 0;
}
buffer[i] = value & 0xff;
value >>>= 8;
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A runtime object which captures compile-time type information.
*
* #### Notes
* A token captures the compile-time type of an interface or class in
* an object which can be used at runtime in a type-safe fashion.
*/
export class Token<T> {
/**
* Construct a new token.
*
* @param name - A human readable name for the token.
* @param description - Token purpose description for documentation.
*/
constructor(name: string, description?: string) {
this.name = name;
this.description = description ?? '';
this._tokenStructuralPropertyT = null!;
}
/**
* Token purpose description.
*/
readonly description?: string; // FIXME remove `?` for the next major version
/**
* The human readable name for the token.
*
* #### Notes
* This can be useful for debugging and logging.
*/
readonly name: string;
private _tokenStructuralPropertyT: T;
}

View File

@@ -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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { uuid4Factory } from './uuid';
import { Random } from './random.browser';
/**
* The namespace for UUID related functionality.
*/
export namespace UUID {
/**
* A function which generates UUID v4 identifiers.
*
* @returns A new UUID v4 string.
*
* #### Notes
* This implementation complies with RFC 4122.
*
* This uses `Random.getRandomValues()` for random bytes, which in
* turn will use the underlying `crypto` module of the platform if
* it is available. The fallback for randomness is `Math.random`.
*/
export const uuid4 = uuid4Factory(Random.getRandomValues);
}

View File

@@ -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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { uuid4Factory } from './uuid';
import { Random } from './random.node';
/**
* The namespace for UUID related functionality.
*/
export namespace UUID {
/**
* A function which generates UUID v4 identifiers.
*
* @returns A new UUID v4 string.
*
* #### Notes
* This implementation complies with RFC 4122.
*
* This uses `Random.getRandomValues()` for random bytes, which in
* turn will use the underlying `crypto` module of the platform if
* it is available. The fallback for randomness is `Math.random`.
*/
export const uuid4 = uuid4Factory(Random.getRandomValues);
}

View File

@@ -0,0 +1,77 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A function which creates a function that generates UUID v4 identifiers.
*
* @returns A new function that creates a UUID v4 string.
*
* #### Notes
* This implementation complies with RFC 4122.
*
* This uses `Random.getRandomValues()` for random bytes, which in
* turn will use the underlying `crypto` module of the platform if
* it is available. The fallback for randomness is `Math.random`.
*/
export function uuid4Factory(
getRandomValues: (bytes: Uint8Array) => void,
): () => string {
// Create a 16 byte array to hold the random values.
const bytes = new Uint8Array(16);
// Create a look up table from bytes to hex strings.
const lut = new Array<string>(256);
// Pad the single character hex digits with a leading zero.
for (let i = 0; i < 16; ++i) {
lut[i] = `0${i.toString(16)}`;
}
// Populate the rest of the hex digits.
for (let i = 16; i < 256; ++i) {
lut[i] = i.toString(16);
}
// Return a function which generates the UUID.
return function uuid4(): string {
// Get a new batch of random values.
getRandomValues(bytes);
// Set the UUID version number to 4.
bytes[6] = 0x40 | (bytes[6] & 0x0f);
// Set the clock sequence bit to the RFC spec.
bytes[8] = 0x80 | (bytes[8] & 0x3f);
// Assemble the UUID string.
return `${lut[bytes[0]] + lut[bytes[1]] + lut[bytes[2]] + lut[bytes[3]]}-${
lut[bytes[4]]
}${lut[bytes[5]]}-${lut[bytes[6]]}${lut[bytes[7]]}-${lut[bytes[8]]}${
lut[bytes[9]]
}-${lut[bytes[10]]}${lut[bytes[11]]}${lut[bytes[12]]}${lut[bytes[13]]}${
lut[bytes[14]]
}${lut[bytes[15]]}`;
};
}

View File

@@ -0,0 +1,277 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module disposable
*/
import { type ISignal, Signal } from '../signaling';
/**
* An object which implements the disposable pattern.
*/
export interface IDisposable {
/**
* Test whether the object has been disposed.
*
* #### Notes
* This property is always safe to access.
*/
readonly isDisposed: boolean;
/**
* Dispose of the resources held by the object.
*
* #### Notes
* If the object's `dispose` method is called more than once, all
* calls made after the first will be a no-op.
*
* #### Undefined Behavior
* It is undefined behavior to use any functionality of the object
* after it has been disposed unless otherwise explicitly noted.
*/
dispose(): void;
}
/**
* A disposable object with an observable `disposed` signal.
*/
export interface IObservableDisposable extends IDisposable {
/**
* A signal emitted when the object is disposed.
*/
readonly disposed: ISignal<this, void>;
}
/**
* A disposable object which delegates to a callback function.
*/
export class DisposableDelegate implements IDisposable {
/**
* Construct a new disposable delegate.
*
* @param fn - The callback function to invoke on dispose.
*/
constructor(fn: () => void) {
this._fn = fn;
}
/**
* Test whether the delegate has been disposed.
*/
get isDisposed(): boolean {
return !this._fn;
}
/**
* Dispose of the delegate and invoke the callback function.
*/
dispose(): void {
if (!this._fn) {
return;
}
const fn = this._fn;
this._fn = null;
fn();
}
private _fn: (() => void) | null;
}
/**
* An observable disposable object which delegates to a callback function.
*/
export class ObservableDisposableDelegate
extends DisposableDelegate
implements IObservableDisposable
{
/**
* A signal emitted when the delegate is disposed.
*/
get disposed(): ISignal<this, void> {
return this._disposed;
}
/**
* Dispose of the delegate and invoke the callback function.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
super.dispose();
this._disposed.emit(undefined);
Signal.clearData(this);
}
private _disposed = new Signal<this, void>(this);
}
/**
* An object which manages a collection of disposable items.
*/
export class DisposableSet implements IDisposable {
/**
* Test whether the set has been disposed.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Dispose of the set and the items it contains.
*
* #### Notes
* Items are disposed in the order they are added to the set.
*/
dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._items.forEach(item => {
item.dispose();
});
this._items.clear();
}
/**
* Test whether the set contains a specific item.
*
* @param item - The item of interest.
*
* @returns `true` if the set contains the item, `false` otherwise.
*/
contains(item: IDisposable): boolean {
return this._items.has(item);
}
/**
* Add a disposable item to the set.
*
* @param item - The item to add to the set.
*
* #### Notes
* If the item is already contained in the set, this is a no-op.
*/
add(item: IDisposable): void {
this._items.add(item);
}
/**
* Remove a disposable item from the set.
*
* @param item - The item to remove from the set.
*
* #### Notes
* If the item is not contained in the set, this is a no-op.
*/
remove(item: IDisposable): void {
this._items.delete(item);
}
/**
* Remove all items from the set.
*/
clear(): void {
this._items.clear();
}
private _isDisposed = false;
private _items = new Set<IDisposable>();
}
/**
* The namespace for the `DisposableSet` class statics.
*/
export namespace DisposableSet {
/**
* Create a disposable set from an iterable of items.
*
* @param items - The iterable object of interest.
*
* @returns A new disposable initialized with the given items.
*/
export function from(items: Iterable<IDisposable>): DisposableSet {
const set = new DisposableSet();
for (const item of items) {
set.add(item);
}
return set;
}
}
/**
* An observable object which manages a collection of disposable items.
*/
export class ObservableDisposableSet
extends DisposableSet
implements IObservableDisposable
{
/**
* A signal emitted when the set is disposed.
*/
get disposed(): ISignal<this, void> {
return this._disposed;
}
/**
* Dispose of the set and the items it contains.
*
* #### Notes
* Items are disposed in the order they are added to the set.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
super.dispose();
this._disposed.emit(undefined);
Signal.clearData(this);
}
private _disposed = new Signal<this, void>(this);
}
/**
* The namespace for the `ObservableDisposableSet` class statics.
*/
export namespace ObservableDisposableSet {
/**
* Create an observable disposable set from an iterable of items.
*
* @param items - The iterable object of interest.
*
* @returns A new disposable initialized with the given items.
*/
export function from(items: Iterable<IDisposable>): ObservableDisposableSet {
const set = new ObservableDisposableSet();
for (const item of items) {
set.add(item);
}
return set;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2019, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* The namespace for clipboard related functionality.
*/
export namespace ClipboardExt {
/**
* Copy text to the system clipboard.
*
* @param text - The text to copy to the clipboard.
*/
export function copyText(text: string): void {
// Fetch the document body.
const { body } = document;
// Set up the clipboard event listener.
const handler = (event: ClipboardEvent) => {
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Set the clipboard data.
event.clipboardData!.setData('text', text);
// Remove the event listener.
body.removeEventListener('copy', handler, true);
};
// Add the event listener.
body.addEventListener('copy', handler, true);
// Trigger the event.
document.execCommand('copy');
}
}

View File

@@ -0,0 +1,229 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* The namespace for element related utilities.
*/
export namespace ElementExt {
/**
* An object which holds the border and padding data for an element.
*/
export interface IBoxSizing {
/**
* The top border width, in pixels.
*/
borderTop: number;
/**
* The left border width, in pixels.
*/
borderLeft: number;
/**
* The right border width, in pixels.
*/
borderRight: number;
/**
* The bottom border width, in pixels.
*/
borderBottom: number;
/**
* The top padding width, in pixels.
*/
paddingTop: number;
/**
* The left padding width, in pixels.
*/
paddingLeft: number;
/**
* The right padding width, in pixels.
*/
paddingRight: number;
/**
* The bottom padding width, in pixels.
*/
paddingBottom: number;
/**
* The sum of horizontal border and padding.
*/
horizontalSum: number;
/**
* The sum of vertical border and padding.
*/
verticalSum: number;
}
/**
* Compute the box sizing for an element.
*
* @param element - The element of interest.
*
* @returns The box sizing data for the specified element.
*/
export function boxSizing(element: Element): IBoxSizing {
const style = window.getComputedStyle(element);
const bt = parseFloat(style.borderTopWidth!) || 0;
const bl = parseFloat(style.borderLeftWidth!) || 0;
const br = parseFloat(style.borderRightWidth!) || 0;
const bb = parseFloat(style.borderBottomWidth!) || 0;
const pt = parseFloat(style.paddingTop!) || 0;
const pl = parseFloat(style.paddingLeft!) || 0;
const pr = parseFloat(style.paddingRight!) || 0;
const pb = parseFloat(style.paddingBottom!) || 0;
const hs = bl + pl + pr + br;
const vs = bt + pt + pb + bb;
return {
borderTop: bt,
borderLeft: bl,
borderRight: br,
borderBottom: bb,
paddingTop: pt,
paddingLeft: pl,
paddingRight: pr,
paddingBottom: pb,
horizontalSum: hs,
verticalSum: vs,
};
}
/**
* An object which holds the min and max size data for an element.
*/
export interface ISizeLimits {
/**
* The minimum width, in pixels.
*/
minWidth: number;
/**
* The minimum height, in pixels.
*/
minHeight: number;
/**
* The maximum width, in pixels.
*/
maxWidth: number;
/**
* The maximum height, in pixels.
*/
maxHeight: number;
}
/**
* Compute the size limits for an element.
*
* @param element - The element of interest.
*
* @returns The size limit data for the specified element.
*/
export function sizeLimits(element: Element): ISizeLimits {
const style = window.getComputedStyle(element);
const minWidth = parseFloat(style.minWidth!) || 0;
const minHeight = parseFloat(style.minHeight!) || 0;
let maxWidth = parseFloat(style.maxWidth!) || Infinity;
let maxHeight = parseFloat(style.maxHeight!) || Infinity;
maxWidth = Math.max(minWidth, maxWidth);
maxHeight = Math.max(minHeight, maxHeight);
return { minWidth, minHeight, maxWidth, maxHeight };
}
/**
* Test whether a client position lies within an element.
*
* @param element - The DOM element of interest.
*
* @param clientX - The client X coordinate of interest.
*
* @param clientY - The client Y coordinate of interest.
*
* @returns Whether the point is within the given element.
*/
export function hitTest(
element: Element,
clientX: number,
clientY: number,
): boolean {
const rect = element.getBoundingClientRect();
return (
clientX >= rect.left &&
clientX < rect.right &&
clientY >= rect.top &&
clientY < rect.bottom
);
}
/**
* Vertically scroll an element into view if needed.
*
* @param area - The scroll area element.
*
* @param element - The element of interest.
*
* #### Notes
* This follows the "nearest" behavior of the native `scrollIntoView`
* method, which is not supported by all browsers.
* https://drafts.csswg.org/cssom-view/#element-scrolling-members
*
* If the element fully covers the visible area or is fully contained
* within the visible area, no scrolling will take place. Otherwise,
* the nearest edges of the area and element are aligned.
*/
export function scrollIntoViewIfNeeded(
area: Element,
element: Element,
): void {
const ar = area.getBoundingClientRect();
const er = element.getBoundingClientRect();
if (er.top <= ar.top && er.bottom >= ar.bottom) {
return;
}
if (er.top < ar.top && er.height <= ar.height) {
area.scrollTop -= ar.top - er.top;
return;
}
if (er.bottom > ar.bottom && er.height >= ar.height) {
area.scrollTop -= ar.top - er.top;
return;
}
if (er.top < ar.top && er.height > ar.height) {
area.scrollTop -= ar.bottom - er.bottom;
return;
}
if (er.bottom > ar.bottom && er.height < ar.height) {
area.scrollTop -= ar.bottom - er.bottom;
return;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module domutils
*/
export * from './clipboard';
export * from './element';
export * from './platform';
export * from './selector';

View File

@@ -0,0 +1,65 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* The namespace for platform related utilities.
*/
export namespace Platform {
/**
* A flag indicating whether the platform is Mac.
*/
export const IS_MAC = !!navigator.platform.match(/Mac/i);
/**
* A flag indicating whether the platform is Windows.
*/
export const IS_WIN = !!navigator.platform.match(/Win/i);
/**
* A flag indicating whether the browser is IE.
*/
export const IS_IE = /Trident/.test(navigator.userAgent);
/**
* A flag indicating whether the browser is Edge.
*/
export const IS_EDGE = /Edge/.test(navigator.userAgent);
/**
* Test whether the `accel` key is pressed.
*
* @param event - The keyboard or mouse event of interest.
*
* @returns Whether the `accel` key is pressed.
*
* #### Notes
* On Mac the `accel` key is the command key. On all other
* platforms the `accel` key is the control key.
*/
export function accelKey(event: KeyboardEvent | MouseEvent): boolean {
return IS_MAC ? event.metaKey : event.ctrlKey;
}
}

View File

@@ -0,0 +1,281 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* The namespace for selector related utilities.
*/
export namespace Selector {
/**
* Calculate the specificity of a single CSS selector.
*
* @param selector - The CSS selector of interest.
*
* @returns The specificity of the selector.
*
* #### Undefined Behavior
* The selector is invalid.
*
* #### Notes
* This is based on https://www.w3.org/TR/css3-selectors/#specificity
*
* A larger number represents a more specific selector.
*
* The smallest possible specificity is `0`.
*
* The result is represented as a hex number `0x<aa><bb><cc>` where
* each component is the count of the respective selector clause.
*
* If the selector contains commas, only the first clause is used.
*
* The computed result is cached, so subsequent calculations for the
* same selector are extremely fast.
*/
export function calculateSpecificity(selector: string): number {
if (selector in Private.specificityCache) {
return Private.specificityCache[selector];
}
const result = Private.calculateSingle(selector);
return (Private.specificityCache[selector] = result);
}
/**
* Test whether a selector is a valid CSS selector.
*
* @param selector - The CSS selector of interest.
*
* @returns `true` if the selector is valid, `false` otherwise.
*
* #### Notes
* The computed result is cached, so subsequent tests for the same
* selector are extremely fast.
*/
export function isValid(selector: string): boolean {
if (selector in Private.validityCache) {
return Private.validityCache[selector];
}
let result = true;
try {
Private.testElem.querySelector(selector);
} catch (err) {
result = false;
}
return (Private.validityCache[selector] = result);
}
/**
* Test whether an element matches a CSS selector.
*
* @param element - The element of interest.
*
* @param selector - The valid CSS selector of interest.
*
* @returns `true` if the element is a match, `false` otherwise.
*
* #### Notes
* This function uses the builtin browser capabilities when possible,
* falling back onto a document query otherwise.
*/
export function matches(element: Element, selector: string): boolean {
return Private.protoMatchFunc.call(element, selector);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A type alias for an object hash.
*/
export interface StringMap<T> {
[key: string]: T;
}
/**
* A cache of computed selector specificity values.
*/
export const specificityCache: StringMap<number> = Object.create(null);
/**
* A cache of computed selector validity.
*/
export const validityCache: StringMap<boolean> = Object.create(null);
/**
* An empty element for testing selector validity.
*/
export const testElem = document.createElement('div');
/**
* A cross-browser CSS selector matching prototype function.
*/
export const protoMatchFunc = (() => {
const proto = Element.prototype as any;
return (
proto.matches ||
proto.matchesSelector ||
proto.mozMatchesSelector ||
proto.msMatchesSelector ||
proto.oMatchesSelector ||
proto.webkitMatchesSelector ||
function (selector: string) {
// @ts-expect-error
const elem = this as Element;
const matches = elem.ownerDocument
? elem.ownerDocument.querySelectorAll(selector)
: [];
return Array.prototype.indexOf.call(matches, elem) !== -1;
}
);
})();
/**
* Calculate the specificity of a single selector.
*
* The behavior is undefined if the selector is invalid.
*/
export function calculateSingle(selector: string): number {
// Ignore anything after the first comma.
selector = selector.split(',', 1)[0];
// Setup the aggregate counters.
let a = 0;
let b = 0;
let c = 0;
// Apply a regex to the front of the selector. If it succeeds, that
// portion of the selector is removed. Returns a success/fail flag.
function match(re: RegExp): boolean {
const match = selector.match(re);
if (match === null) {
return false;
}
selector = selector.slice(match[0].length);
return true;
}
// Replace the negation pseudo-class (which is ignored),
// but keep its inner content (which is not ignored).
selector = selector.replace(NEGATION_RE, ' $1 ');
// Continue matching until the selector is consumed.
while (selector.length > 0) {
// Match an ID selector.
if (match(ID_RE)) {
a++;
continue;
}
// Match a class selector.
if (match(CLASS_RE)) {
b++;
continue;
}
// Match an attribute selector.
if (match(ATTR_RE)) {
b++;
continue;
}
// Match a pseudo-element selector. This is done before matching
// a pseudo-class since this regex overlaps with that regex.
if (match(PSEUDO_ELEM_RE)) {
c++;
continue;
}
// Match a pseudo-class selector.
if (match(PSEDUO_CLASS_RE)) {
b++;
continue;
}
// Match a plain type selector.
if (match(TYPE_RE)) {
c++;
continue;
}
// Finally, match any ignored characters.
if (match(IGNORE_RE)) {
continue;
}
// At this point, the selector is assumed to be invalid.
return 0;
}
// Clamp each component to a reasonable base.
a = Math.min(a, 0xff);
b = Math.min(b, 0xff);
c = Math.min(c, 0xff);
// Combine the components into a single result.
return (a << 16) | (b << 8) | c;
}
/**
* A regex which matches an ID selector at string start.
*/
const ID_RE = /^#[^\s\+>~#\.\[:]+/;
/**
* A regex which matches a class selector at string start.
*/
const CLASS_RE = /^\.[^\s\+>~#\.\[:]+/;
/**
* A regex which matches an attribute selector at string start.
*/
const ATTR_RE = /^\[[^\]]+\]/;
/**
* A regex which matches a type selector at string start.
*/
const TYPE_RE = /^[^\s\+>~#\.\[:]+/;
/**
* A regex which matches a pseudo-element selector at string start.
*/
const PSEUDO_ELEM_RE =
/^(::[^\s\+>~#\.\[:]+|:first-line|:first-letter|:before|:after)/;
/**
* A regex which matches a pseudo-class selector at string start.
*/
const PSEDUO_CLASS_RE = /^:[^\s\+>~#\.\[:]+/;
/**
* A regex which matches ignored characters at string start.
*/
const IGNORE_RE = /^[\s\+>~\*]+/;
/**
* A regex which matches the negation pseudo-class globally.
*/
const NEGATION_RE = /:not\(([^\)]+)\)/g;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,659 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module messaging
*/
import { LinkedList } from '../collections';
import { ArrayExt, every, retro, some } from '../algorithm';
/**
* A message which can be delivered to a message handler.
*
* #### Notes
* This class may be subclassed to create complex message types.
*/
export class Message {
/**
* Construct a new message.
*
* @param type - The type of the message.
*/
constructor(type: string) {
this.type = type;
}
/**
* The type of the message.
*
* #### Notes
* The `type` of a message should be related directly to its actual
* runtime type. This means that `type` can and will be used to cast
* the message to the relevant derived `Message` subtype.
*/
readonly type: string;
/**
* Test whether the message is conflatable.
*
* #### Notes
* Message conflation is an advanced topic. Most message types will
* not make use of this feature.
*
* If a conflatable message is posted to a handler while another
* conflatable message of the same `type` has already been posted
* to the handler, the `conflate()` method of the existing message
* will be invoked. If that method returns `true`, the new message
* will not be enqueued. This allows messages to be compressed, so
* that only a single instance of the message type is processed per
* cycle, no matter how many times messages of that type are posted.
*
* Custom message types may reimplement this property.
*
* The default implementation is always `false`.
*/
get isConflatable(): boolean {
return false;
}
/**
* Conflate this message with another message of the same `type`.
*
* @param other - A conflatable message of the same `type`.
*
* @returns `true` if the message was successfully conflated, or
* `false` otherwise.
*
* #### Notes
* Message conflation is an advanced topic. Most message types will
* not make use of this feature.
*
* This method is called automatically by the message loop when the
* given message is posted to the handler paired with this message.
* This message will already be enqueued and conflatable, and the
* given message will have the same `type` and also be conflatable.
*
* This method should merge the state of the other message into this
* message as needed so that when this message is finally delivered
* to the handler, it receives the most up-to-date information.
*
* If this method returns `true`, it signals that the other message
* was successfully conflated and that message will not be enqueued.
*
* If this method returns `false`, the other message will be enqueued
* for normal delivery.
*
* Custom message types may reimplement this method.
*
* The default implementation always returns `false`.
*/
conflate(other: Message): boolean {
return false;
}
}
/**
* A convenience message class which conflates automatically.
*
* #### Notes
* Message conflation is an advanced topic. Most user code will not
* make use of this class.
*
* This message class is useful for creating message instances which
* should be conflated, but which have no state other than `type`.
*
* If conflation of stateful messages is required, a custom `Message`
* subclass should be created.
*/
export class ConflatableMessage extends Message {
/**
* Test whether the message is conflatable.
*
* #### Notes
* This property is always `true`.
*/
get isConflatable(): boolean {
return true;
}
/**
* Conflate this message with another message of the same `type`.
*
* #### Notes
* This method always returns `true`.
*/
conflate(other: ConflatableMessage): boolean {
return true;
}
}
/**
* An object which handles messages.
*
* #### Notes
* A message handler is a simple way of defining a type which can act
* upon on a large variety of external input without requiring a large
* abstract API surface. This is particularly useful in the context of
* widget frameworks where the number of distinct message types can be
* unbounded.
*/
export interface IMessageHandler {
/**
* Process a message sent to the handler.
*
* @param msg - The message to be processed.
*/
processMessage(msg: Message): void;
}
/**
* An object which intercepts messages sent to a message handler.
*
* #### Notes
* A message hook is useful for intercepting or spying on messages
* sent to message handlers which were either not created by the
* consumer, or when subclassing the handler is not feasible.
*
* If `messageHook` returns `false`, no other message hooks will be
* invoked and the message will not be delivered to the handler.
*
* If all installed message hooks return `true`, the message will
* be delivered to the handler for processing.
*
* **See also:** {@link MessageLoop.installMessageHook} and {@link MessageLoop.removeMessageHook}
*/
export interface IMessageHook {
/**
* Intercept a message sent to a message handler.
*
* @param handler - The target handler of the message.
*
* @param msg - The message to be sent to the handler.
*
* @returns `true` if the message should continue to be processed
* as normal, or `false` if processing should cease immediately.
*/
messageHook(handler: IMessageHandler, msg: Message): boolean;
}
/**
* A type alias for message hook object or function.
*
* #### Notes
* The signature and semantics of a message hook function are the same
* as the `messageHook` method of {@link IMessageHook}.
*/
export type MessageHook =
| IMessageHook
| ((handler: IMessageHandler, msg: Message) => boolean);
/**
* The namespace for the global singleton message loop.
*/
export namespace MessageLoop {
/**
* A function that cancels the pending loop task; `null` if unavailable.
*/
let pending: (() => void) | null = null;
/**
* Schedules a function for invocation as soon as possible asynchronously.
*
* @param fn The function to invoke when called back.
*
* @returns An anonymous function that will unschedule invocation if possible.
*/
const schedule = (
resolved =>
(fn: () => unknown): (() => void) => {
let rejected = false;
resolved.then(() => !rejected && fn());
return () => {
rejected = true;
};
}
)(Promise.resolve());
/**
* Send a message to a message handler to process immediately.
*
* @param handler - The handler which should process the message.
*
* @param msg - The message to deliver to the handler.
*
* #### Notes
* The message will first be sent through any installed message hooks
* for the handler. If the message passes all hooks, it will then be
* delivered to the `processMessage` method of the handler.
*
* The message will not be conflated with pending posted messages.
*
* Exceptions in hooks and handlers will be caught and logged.
*/
export function sendMessage(handler: IMessageHandler, msg: Message): void {
// Lookup the message hooks for the handler.
const hooks = messageHooks.get(handler);
// Handle the common case of no installed hooks.
if (!hooks || hooks.length === 0) {
invokeHandler(handler, msg);
return;
}
// Invoke the message hooks starting with the newest first.
const passed = every(retro(hooks), hook =>
hook ? invokeHook(hook, handler, msg) : true,
);
// Invoke the handler if the message passes all hooks.
if (passed) {
invokeHandler(handler, msg);
}
}
/**
* Post a message to a message handler to process in the future.
*
* @param handler - The handler which should process the message.
*
* @param msg - The message to post to the handler.
*
* #### Notes
* The message will be conflated with the pending posted messages for
* the handler, if possible. If the message is not conflated, it will
* be queued for normal delivery on the next cycle of the event loop.
*
* Exceptions in hooks and handlers will be caught and logged.
*/
export function postMessage(handler: IMessageHandler, msg: Message): void {
// Handle the common case of a non-conflatable message.
if (!msg.isConflatable) {
enqueueMessage(handler, msg);
return;
}
// Conflate the message with an existing message if possible.
const conflated = some(messageQueue, posted => {
if (posted.handler !== handler) {
return false;
}
if (!posted.msg) {
return false;
}
if (posted.msg.type !== msg.type) {
return false;
}
if (!posted.msg.isConflatable) {
return false;
}
return posted.msg.conflate(msg);
});
// Enqueue the message if it was not conflated.
if (!conflated) {
enqueueMessage(handler, msg);
}
}
/**
* Install a message hook for a message handler.
*
* @param handler - The message handler of interest.
*
* @param hook - The message hook to install.
*
* #### Notes
* A message hook is invoked before a message is delivered to the
* handler. If the hook returns `false`, no other hooks will be
* invoked and the message will not be delivered to the handler.
*
* The most recently installed message hook is executed first.
*
* If the hook is already installed, this is a no-op.
*/
export function installMessageHook(
handler: IMessageHandler,
hook: MessageHook,
): void {
// Look up the hooks for the handler.
const hooks = messageHooks.get(handler);
// Bail early if the hook is already installed.
if (hooks && hooks.indexOf(hook) !== -1) {
return;
}
// Add the hook to the end, so it will be the first to execute.
if (!hooks) {
messageHooks.set(handler, [hook]);
} else {
hooks.push(hook);
}
}
/**
* Remove an installed message hook for a message handler.
*
* @param handler - The message handler of interest.
*
* @param hook - The message hook to remove.
*
* #### Notes
* It is safe to call this function while the hook is executing.
*
* If the hook is not installed, this is a no-op.
*/
export function removeMessageHook(
handler: IMessageHandler,
hook: MessageHook,
): void {
// Lookup the hooks for the handler.
const hooks = messageHooks.get(handler);
// Bail early if the hooks do not exist.
if (!hooks) {
return;
}
// Lookup the index of the hook and bail if not found.
const i = hooks.indexOf(hook);
if (i === -1) {
return;
}
// Clear the hook and schedule a cleanup of the array.
hooks[i] = null;
scheduleCleanup(hooks);
}
/**
* Clear all message data associated with a message handler.
*
* @param handler - The message handler of interest.
*
* #### Notes
* This will clear all posted messages and hooks for the handler.
*/
export function clearData(handler: IMessageHandler): void {
// Lookup the hooks for the handler.
const hooks = messageHooks.get(handler);
// Clear all messsage hooks for the handler.
if (hooks && hooks.length > 0) {
ArrayExt.fill(hooks, null);
scheduleCleanup(hooks);
}
// Clear all posted messages for the handler.
for (const posted of messageQueue) {
if (posted.handler === handler) {
posted.handler = null;
posted.msg = null;
}
}
}
/**
* Process the pending posted messages in the queue immediately.
*
* #### Notes
* This function is useful when posted messages must be processed immediately.
*
* This function should normally not be needed, but it may be
* required to work around certain browser idiosyncrasies.
*
* Recursing into this function is a no-op.
*/
export function flush(): void {
// Bail if recursion is detected or if there is no pending task.
if (flushGuard || pending === null) {
return;
}
// Unschedule the pending loop task.
pending();
pending = null;
// Run the message loop within the recursion guard.
flushGuard = true;
runMessageLoop();
flushGuard = false;
}
/**
* A type alias for the exception handler function.
*/
export type ExceptionHandler = (err: Error) => void;
/**
* Get the message loop exception handler.
*
* @returns The current exception handler.
*
* #### Notes
* The default exception handler is `console.error`.
*/
export function getExceptionHandler(): ExceptionHandler {
return exceptionHandler;
}
/**
* Set the message loop exception handler.
*
* @param handler - The function to use as the exception handler.
*
* @returns The old exception handler.
*
* #### Notes
* The exception handler is invoked when a message handler or a
* message hook throws an exception.
*/
export function setExceptionHandler(
handler: ExceptionHandler,
): ExceptionHandler {
const old = exceptionHandler;
exceptionHandler = handler;
return old;
}
/**
* A type alias for a posted message pair.
*/
interface PostedMessage {
handler: IMessageHandler | null;
msg: Message | null;
}
/**
* The queue of posted message pairs.
*/
const messageQueue = new LinkedList<PostedMessage>();
/**
* A mapping of handler to array of installed message hooks.
*/
const messageHooks = new WeakMap<
IMessageHandler,
Array<MessageHook | null>
>();
/**
* A set of message hook arrays which are pending cleanup.
*/
const dirtySet = new Set<Array<MessageHook | null>>();
/**
* The message loop exception handler.
*/
let exceptionHandler: ExceptionHandler = (err: Error) => {
console.error(err);
};
/**
* A guard flag to prevent flush recursion.
*/
let flushGuard = false;
/**
* Invoke a message hook with the specified handler and message.
*
* Returns the result of the hook, or `true` if the hook throws.
*
* Exceptions in the hook will be caught and logged.
*/
function invokeHook(
hook: MessageHook,
handler: IMessageHandler,
msg: Message,
): boolean {
let result = true;
try {
if (typeof hook === 'function') {
result = hook(handler, msg);
} else {
result = hook.messageHook(handler, msg);
}
} catch (err) {
exceptionHandler(err as Error);
}
return result;
}
/**
* Invoke a message handler with the specified message.
*
* Exceptions in the handler will be caught and logged.
*/
function invokeHandler(handler: IMessageHandler, msg: Message): void {
try {
handler.processMessage(msg);
} catch (err) {
exceptionHandler(err as Error);
}
}
/**
* Add a message to the end of the message queue.
*
* This will automatically schedule a run of the message loop.
*/
function enqueueMessage(handler: IMessageHandler, msg: Message): void {
// Add the posted message to the queue.
messageQueue.addLast({ handler, msg });
// Bail if a loop task is already pending.
if (pending !== null) {
return;
}
// Schedule a run of the message loop.
pending = schedule(runMessageLoop);
}
/**
* Run an iteration of the message loop.
*
* This will process all pending messages in the queue. If a message
* is added to the queue while the message loop is running, it will
* be processed on the next cycle of the loop.
*/
function runMessageLoop(): void {
// Clear the task so the next loop can be scheduled.
pending = null;
// If the message queue is empty, there is nothing else to do.
if (messageQueue.isEmpty) {
return;
}
// Add a sentinel value to the end of the queue. The queue will
// only be processed up to the sentinel. Messages posted during
// this cycle will execute on the next cycle.
const sentinel: PostedMessage = { handler: null, msg: null };
messageQueue.addLast(sentinel);
// Enter the message loop.
while (true) {
// Remove the first posted message in the queue.
const posted = messageQueue.removeFirst()!;
// If the value is the sentinel, exit the loop.
if (posted === sentinel) {
return;
}
// Dispatch the message if it has not been cleared.
if (posted.handler && posted.msg) {
sendMessage(posted.handler, posted.msg);
}
}
}
/**
* Schedule a cleanup of a message hooks array.
*
* This will add the array to the dirty set and schedule a deferred
* cleanup of the array contents. On cleanup, any `null` hook will
* be removed from the array.
*/
function scheduleCleanup(hooks: Array<MessageHook | null>): void {
if (dirtySet.size === 0) {
schedule(cleanupDirtySet);
}
dirtySet.add(hooks);
}
/**
* Cleanup the message hook arrays in the dirty set.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupDirtySet(): void {
dirtySet.forEach(cleanupHooks);
dirtySet.clear();
}
/**
* Cleanup the dirty hooks in a message hooks array.
*
* This will remove any `null` hook from the array.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupHooks(hooks: Array<MessageHook | null>): void {
ArrayExt.removeAllWhere(hooks, isNull);
}
/**
* Test whether a value is `null`.
*/
function isNull<T>(value: T | null): boolean {
return value === null;
}
}

View File

@@ -0,0 +1,297 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module properties
*/
/**
* A class which attaches a value to an external object.
*
* #### Notes
* Attached properties are used to extend the state of an object with
* semantic data from an unrelated class. They also encapsulate value
* creation, coercion, and notification.
*
* Because attached property values are stored in a hash table, which
* in turn is stored in a WeakMap keyed on the owner object, there is
* non-trivial storage overhead involved in their use. The pattern is
* therefore best used for the storage of rare data.
*/
export class AttachedProperty<T, U> {
/**
* Construct a new attached property.
*
* @param options - The options for initializing the property.
*/
constructor(options: AttachedProperty.IOptions<T, U>) {
this.name = options.name;
this._create = options.create;
this._coerce = options.coerce || null;
this._compare = options.compare || null;
this._changed = options.changed || null;
}
/**
* The human readable name for the property.
*/
readonly name: string;
/**
* Get the current value of the property for a given owner.
*
* @param owner - The property owner of interest.
*
* @returns The current value of the property.
*
* #### Notes
* If the value has not yet been set, the default value will be
* computed and assigned as the current value of the property.
*/
get(owner: T): U {
let value: U;
const map = Private.ensureMap(owner);
if (this._pid in map) {
value = map[this._pid];
} else {
value = map[this._pid] = this._createValue(owner);
}
return value;
}
/**
* Set the current value of the property for a given owner.
*
* @param owner - The property owner of interest.
*
* @param value - The value for the property.
*
* #### Notes
* If the value has not yet been set, the default value will be
* computed and used as the previous value for the comparison.
*/
set(owner: T, value: U): void {
let oldValue: U;
const map = Private.ensureMap(owner);
if (this._pid in map) {
oldValue = map[this._pid];
} else {
oldValue = map[this._pid] = this._createValue(owner);
}
const newValue = this._coerceValue(owner, value);
this._maybeNotify(owner, oldValue, (map[this._pid] = newValue));
}
/**
* Explicitly coerce the current property value for a given owner.
*
* @param owner - The property owner of interest.
*
* #### Notes
* If the value has not yet been set, the default value will be
* computed and used as the previous value for the comparison.
*/
coerce(owner: T): void {
let oldValue: U;
const map = Private.ensureMap(owner);
if (this._pid in map) {
oldValue = map[this._pid];
} else {
oldValue = map[this._pid] = this._createValue(owner);
}
const newValue = this._coerceValue(owner, oldValue);
this._maybeNotify(owner, oldValue, (map[this._pid] = newValue));
}
/**
* Get or create the default value for the given owner.
*/
private _createValue(owner: T): U {
const create = this._create;
return create(owner);
}
/**
* Coerce the value for the given owner.
*/
private _coerceValue(owner: T, value: U): U {
const coerce = this._coerce;
return coerce ? coerce(owner, value) : value;
}
/**
* Compare the old value and new value for equality.
*/
private _compareValue(oldValue: U, newValue: U): boolean {
const compare = this._compare;
return compare ? compare(oldValue, newValue) : oldValue === newValue;
}
/**
* Run the change notification if the given values are different.
*/
private _maybeNotify(owner: T, oldValue: U, newValue: U): void {
const changed = this._changed;
if (changed && !this._compareValue(oldValue, newValue)) {
changed(owner, oldValue, newValue);
}
}
private _pid = Private.nextPID();
private _create: (owner: T) => U;
private _coerce: ((owner: T, value: U) => U) | null;
private _compare: ((oldValue: U, newValue: U) => boolean) | null;
private _changed: ((owner: T, oldValue: U, newValue: U) => void) | null;
}
/**
* The namespace for the `AttachedProperty` class statics.
*/
export namespace AttachedProperty {
/**
* The options object used to initialize an attached property.
*/
export interface IOptions<T, U> {
/**
* The human readable name for the property.
*
* #### Notes
* By convention, this should be the same as the name used to define
* the public accessor for the property value.
*
* This **does not** have an effect on the property lookup behavior.
* Multiple properties may share the same name without conflict.
*/
name: string;
/**
* A factory function used to create the default property value.
*
* #### Notes
* This will be called whenever the property value is required,
* but has not yet been set for a given owner.
*/
create: (owner: T) => U;
/**
* A function used to coerce a supplied value into the final value.
*
* #### Notes
* This will be called whenever the property value is changed, or
* when the property is explicitly coerced. The return value will
* be used as the final value of the property.
*
* This will **not** be called for the initial default value.
*/
coerce?: (owner: T, value: U) => U;
/**
* A function used to compare two values for equality.
*
* #### Notes
* This is called to determine if the property value has changed.
* It should return `true` if the given values are equivalent, or
* `false` if they are different.
*
* If this is not provided, it defaults to the `===` operator.
*/
compare?: (oldValue: U, newValue: U) => boolean;
/**
* A function called when the property value has changed.
*
* #### Notes
* This will be invoked when the property value is changed and the
* comparator indicates that the old value is not equal to the new
* value.
*
* This will **not** be called for the initial default value.
*/
changed?: (owner: T, oldValue: U, newValue: U) => void;
}
/**
* Clear the stored property data for the given owner.
*
* @param owner - The property owner of interest.
*
* #### Notes
* This will clear all property values for the owner, but it will
* **not** run the change notification for any of the properties.
*/
export function clearData(owner: unknown): void {
Private.ownerData.delete(owner);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A typedef for a mapping of property id to property value.
*/
export interface PropertyMap {
[key: string]: any;
}
/**
* A weak mapping of property owner to property map.
*/
export const ownerData = new WeakMap<any, PropertyMap>();
/**
* A function which computes successive unique property ids.
*/
export const nextPID = (() => {
let id = 0;
return () => {
const rand = Math.random();
const stem = `${rand}`.slice(2);
return `pid-${stem}-${id++}`;
};
})();
/**
* Lookup the data map for the property owner.
*
* This will create the map if one does not already exist.
*/
export function ensureMap(owner: unknown): PropertyMap {
let map = ownerData.get(owner);
if (map) {
return map;
}
map = Object.create(null) as PropertyMap;
ownerData.set(owner, map);
return map;
}
}

View File

@@ -0,0 +1,785 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module signaling
*/
import { PromiseDelegate } from '../coreutils';
import { ArrayExt, find } from '../algorithm';
/**
* A type alias for a slot function.
*
* @param sender - The object emitting the signal.
*
* @param args - The args object emitted with the signal.
*
* #### Notes
* A slot is invoked when a signal to which it is connected is emitted.
*/
export type Slot<T, U> = (sender: T, args: U) => void;
/**
* An object used for type-safe inter-object communication.
*
* #### Notes
* Signals provide a type-safe implementation of the publish-subscribe
* pattern. An object (publisher) declares which signals it will emit,
* and consumers connect callbacks (subscribers) to those signals. The
* subscribers are invoked whenever the publisher emits the signal.
*/
export interface ISignal<T, U> {
/**
* Connect a slot to the signal.
*
* @param slot - The slot to invoke when the signal is emitted.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection succeeds, `false` otherwise.
*
* #### Notes
* Slots are invoked in the order in which they are connected.
*
* Signal connections are unique. If a connection already exists for
* the given `slot` and `thisArg`, this method returns `false`.
*
* A newly connected slot will not be invoked until the next time the
* signal is emitted, even if the slot is connected while the signal
* is dispatching.
*/
connect(slot: Slot<T, U>, thisArg?: any): boolean;
/**
* Disconnect a slot from the signal.
*
* @param slot - The slot to disconnect from the signal.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection is removed, `false` otherwise.
*
* #### Notes
* If no connection exists for the given `slot` and `thisArg`, this
* method returns `false`.
*
* A disconnected slot will no longer be invoked, even if the slot
* is disconnected while the signal is dispatching.
*/
disconnect(slot: Slot<T, U>, thisArg?: any): boolean;
}
/**
* An object that is both a signal and an async iterable.
*/
export interface IStream<T, U> extends ISignal<T, U>, AsyncIterable<U> {}
/**
* A concrete implementation of `ISignal`.
*
* #### Example
* ```typescript
* import { ISignal, Signal } from '../signaling';
*
* class SomeClass {
*
* constructor(name: string) {
* this.name = name;
* }
*
* readonly name: string;
*
* get valueChanged: ISignal<this, number> {
* return this._valueChanged;
* }
*
* get value(): number {
* return this._value;
* }
*
* set value(value: number) {
* if (value === this._value) {
* return;
* }
* this._value = value;
* this._valueChanged.emit(value);
* }
*
* private _value = 0;
* private _valueChanged = new Signal<this, number>(this);
* }
*
* function logger(sender: SomeClass, value: number): void {
* console.log(sender.name, value);
* }
*
* let m1 = new SomeClass('foo');
* let m2 = new SomeClass('bar');
*
* m1.valueChanged.connect(logger);
* m2.valueChanged.connect(logger);
*
* m1.value = 42; // logs: foo 42
* m2.value = 17; // logs: bar 17
* ```
*/
export class Signal<T, U> implements ISignal<T, U> {
/**
* Construct a new signal.
*
* @param sender - The sender which owns the signal.
*/
constructor(sender: T) {
this.sender = sender;
}
/**
* The sender which owns the signal.
*/
readonly sender: T;
/**
* Connect a slot to the signal.
*
* @param slot - The slot to invoke when the signal is emitted.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection succeeds, `false` otherwise.
*/
connect(slot: Slot<T, U>, thisArg?: unknown): boolean {
return Private.connect(this, slot, thisArg);
}
/**
* Disconnect a slot from the signal.
*
* @param slot - The slot to disconnect from the signal.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection is removed, `false` otherwise.
*/
disconnect(slot: Slot<T, U>, thisArg?: unknown): boolean {
return Private.disconnect(this, slot, thisArg);
}
/**
* Emit the signal and invoke the connected slots.
*
* @param args - The args to pass to the connected slots.
*
* #### Notes
* Slots are invoked synchronously in connection order.
*
* Exceptions thrown by connected slots will be caught and logged.
*/
emit(args: U): void {
Private.emit(this, args);
}
}
/**
* The namespace for the `Signal` class statics.
*/
export namespace Signal {
/**
* Remove all connections between a sender and receiver.
*
* @param sender - The sender object of interest.
*
* @param receiver - The receiver object of interest.
*
* #### Notes
* If a `thisArg` is provided when connecting a signal, that object
* is considered the receiver. Otherwise, the `slot` is considered
* the receiver.
*/
export function disconnectBetween(sender: unknown, receiver: unknown): void {
Private.disconnectBetween(sender, receiver);
}
/**
* Remove all connections where the given object is the sender.
*
* @param sender - The sender object of interest.
*/
export function disconnectSender(sender: unknown): void {
Private.disconnectSender(sender);
}
/**
* Remove all connections where the given object is the receiver.
*
* @param receiver - The receiver object of interest.
*
* #### Notes
* If a `thisArg` is provided when connecting a signal, that object
* is considered the receiver. Otherwise, the `slot` is considered
* the receiver.
*/
export function disconnectReceiver(receiver: unknown): void {
Private.disconnectReceiver(receiver);
}
/**
* Remove all connections where an object is the sender or receiver.
*
* @param object - The object of interest.
*
* #### Notes
* If a `thisArg` is provided when connecting a signal, that object
* is considered the receiver. Otherwise, the `slot` is considered
* the receiver.
*/
export function disconnectAll(object: unknown): void {
Private.disconnectAll(object);
}
/**
* Clear all signal data associated with the given object.
*
* @param object - The object for which the data should be cleared.
*
* #### Notes
* This removes all signal connections and any other signal data
* associated with the object.
*/
export function clearData(object: unknown): void {
Private.disconnectAll(object);
}
/**
* A type alias for the exception handler function.
*/
export type ExceptionHandler = (err: Error) => void;
/**
* Get the signal exception handler.
*
* @returns The current exception handler.
*
* #### Notes
* The default exception handler is `console.error`.
*/
export function getExceptionHandler(): ExceptionHandler {
return Private.exceptionHandler;
}
/**
* Set the signal exception handler.
*
* @param handler - The function to use as the exception handler.
*
* @returns The old exception handler.
*
* #### Notes
* The exception handler is invoked when a slot throws an exception.
*/
export function setExceptionHandler(
handler: ExceptionHandler,
): ExceptionHandler {
const old = Private.exceptionHandler;
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore
Private.exceptionHandler = handler;
return old;
}
}
/**
* A concrete implementation of `IStream`.
*
* #### Example
* ```typescript
* import { IStream, Stream } from '../signaling';
*
* class SomeClass {
*
* constructor(name: string) {
* this.name = name;
* }
*
* readonly name: string;
*
* get pings(): IStream<this, string> {
* return this._pings;
* }
*
* ping(value: string) {
* this._pings.emit(value);
* }
*
* private _pings = new Stream<this, string>(this);
* }
*
* let m1 = new SomeClass('foo');
*
* m1.pings.connect((_, value: string) => {
* console.log('connect', value);
* });
*
* void (async () => {
* for await (const ping of m1.pings) {
* console.log('iterator', ping);
* }
* })();
*
* m1.ping('alpha'); // logs: connect alpha
* // logs: iterator alpha
* m1.ping('beta'); // logs: connect beta
* // logs: iterator beta
* ```
*/
export class Stream<T, U> extends Signal<T, U> implements IStream<T, U> {
/**
* Return an async iterator that yields every emission.
*/
async *[Symbol.asyncIterator](): AsyncIterableIterator<U> {
let pending = this._pending;
while (true) {
try {
const { args, next } = await pending.promise;
pending = next;
yield args;
} catch (_) {
return; // Any promise rejection stops the iterator.
}
}
}
/**
* Emit the signal, invoke the connected slots, and yield the emission.
*
* @param args - The args to pass to the connected slots.
*/
emit(args: U): void {
const pending = this._pending;
const next = (this._pending = new PromiseDelegate());
pending.resolve({ args, next });
super.emit(args);
}
/**
* Stop the stream's async iteration.
*/
stop(): void {
this._pending.promise.catch(() => undefined);
this._pending.reject('stop');
this._pending = new PromiseDelegate();
}
private _pending: Private.Pending<U> = new PromiseDelegate();
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A pending promise in a promise chain underlying a stream.
*/
export type Pending<U> = PromiseDelegate<{ args: U; next: Pending<U> }>;
/**
* The signal exception handler function.
*/
export const exceptionHandler: Signal.ExceptionHandler = (err: Error) => {
console.error(err);
};
/**
* Connect a slot to a signal.
*
* @param signal - The signal of interest.
*
* @param slot - The slot to invoke when the signal is emitted.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection succeeds, `false` otherwise.
*/
export function connect<T, U>(
signal: Signal<T, U>,
slot: Slot<T, U>,
thisArg?: unknown,
): boolean {
// Coerce a `null` `thisArg` to `undefined`.
thisArg = thisArg || undefined;
// Ensure the sender's array of receivers is created.
let receivers = receiversForSender.get(signal.sender);
if (!receivers) {
receivers = [];
receiversForSender.set(signal.sender, receivers);
}
// Bail if a matching connection already exists.
if (findConnection(receivers, signal, slot, thisArg)) {
return false;
}
// Choose the best object for the receiver.
const receiver = thisArg || slot;
// Ensure the receiver's array of senders is created.
let senders = sendersForReceiver.get(receiver);
if (!senders) {
senders = [];
sendersForReceiver.set(receiver, senders);
}
// Create a new connection and add it to the end of each array.
const connection = { signal, slot, thisArg };
receivers.push(connection);
senders.push(connection);
// Indicate a successful connection.
return true;
}
/**
* Disconnect a slot from a signal.
*
* @param signal - The signal of interest.
*
* @param slot - The slot to disconnect from the signal.
*
* @param thisArg - The `this` context for the slot. If provided,
* this must be a non-primitive object.
*
* @returns `true` if the connection is removed, `false` otherwise.
*/
export function disconnect<T, U>(
signal: Signal<T, U>,
slot: Slot<T, U>,
thisArg?: unknown,
): boolean {
// Coerce a `null` `thisArg` to `undefined`.
thisArg = thisArg || undefined;
// Lookup the list of receivers, and bail if none exist.
const receivers = receiversForSender.get(signal.sender);
if (!receivers || receivers.length === 0) {
return false;
}
// Bail if no matching connection exits.
const connection = findConnection(receivers, signal, slot, thisArg);
if (!connection) {
return false;
}
// Choose the best object for the receiver.
const receiver = thisArg || slot;
// Lookup the array of senders, which is now known to exist.
const senders = sendersForReceiver.get(receiver)!;
// Clear the connection and schedule cleanup of the arrays.
connection.signal = null;
scheduleCleanup(receivers);
scheduleCleanup(senders);
// Indicate a successful disconnection.
return true;
}
/**
* Remove all connections between a sender and receiver.
*
* @param sender - The sender object of interest.
*
* @param receiver - The receiver object of interest.
*/
export function disconnectBetween(sender: unknown, receiver: unknown): void {
// If there are no receivers, there is nothing to do.
const receivers = receiversForSender.get(sender);
if (!receivers || receivers.length === 0) {
return;
}
// If there are no senders, there is nothing to do.
const senders = sendersForReceiver.get(receiver);
if (!senders || senders.length === 0) {
return;
}
// Clear each connection between the sender and receiver.
for (const connection of senders) {
// Skip connections which have already been cleared.
if (!connection.signal) {
continue;
}
// Clear the connection if it matches the sender.
if (connection.signal.sender === sender) {
connection.signal = null;
}
}
// Schedule a cleanup of the senders and receivers.
scheduleCleanup(receivers);
scheduleCleanup(senders);
}
/**
* Remove all connections where the given object is the sender.
*
* @param sender - The sender object of interest.
*/
export function disconnectSender(sender: unknown): void {
// If there are no receivers, there is nothing to do.
const receivers = receiversForSender.get(sender);
if (!receivers || receivers.length === 0) {
return;
}
// Clear each receiver connection.
for (const connection of receivers) {
// Skip connections which have already been cleared.
if (!connection.signal) {
continue;
}
// Choose the best object for the receiver.
const receiver = connection.thisArg || connection.slot;
// Clear the connection.
connection.signal = null;
// Cleanup the array of senders, which is now known to exist.
scheduleCleanup(sendersForReceiver.get(receiver)!);
}
// Schedule a cleanup of the receivers.
scheduleCleanup(receivers);
}
/**
* Remove all connections where the given object is the receiver.
*
* @param receiver - The receiver object of interest.
*/
export function disconnectReceiver(receiver: unknown): void {
// If there are no senders, there is nothing to do.
const senders = sendersForReceiver.get(receiver);
if (!senders || senders.length === 0) {
return;
}
// Clear each sender connection.
for (const connection of senders) {
// Skip connections which have already been cleared.
if (!connection.signal) {
continue;
}
// Lookup the sender for the connection.
const { sender } = connection.signal;
// Clear the connection.
connection.signal = null;
// Cleanup the array of receivers, which is now known to exist.
scheduleCleanup(receiversForSender.get(sender)!);
}
// Schedule a cleanup of the list of senders.
scheduleCleanup(senders);
}
/**
* Remove all connections where an object is the sender or receiver.
*
* @param object - The object of interest.
*/
export function disconnectAll(object: unknown): void {
// Remove all connections where the given object is the sender.
disconnectSender(object);
// Remove all connections where the given object is the receiver.
disconnectReceiver(object);
}
/**
* Emit a signal and invoke its connected slots.
*
* @param signal - The signal of interest.
*
* @param args - The args to pass to the connected slots.
*
* #### Notes
* Slots are invoked synchronously in connection order.
*
* Exceptions thrown by connected slots will be caught and logged.
*/
export function emit<T, U>(signal: Signal<T, U>, args: U): void {
// If there are no receivers, there is nothing to do.
const receivers = receiversForSender.get(signal.sender);
if (!receivers || receivers.length === 0) {
return;
}
// Invoke the slots for connections with a matching signal.
// Any connections added during emission are not invoked.
for (let i = 0, n = receivers.length; i < n; ++i) {
const connection = receivers[i];
if (connection.signal === signal) {
invokeSlot(connection, args);
}
}
}
/**
* An object which holds connection data.
*/
interface IConnection {
/**
* The signal for the connection.
*
* A `null` signal indicates a cleared connection.
*/
signal: Signal<any, any> | null;
/**
* The slot connected to the signal.
*/
readonly slot: Slot<any, any>;
/**
* The `this` context for the slot.
*/
readonly thisArg: any;
}
/**
* A weak mapping of sender to array of receiver connections.
*/
const receiversForSender = new WeakMap<any, IConnection[]>();
/**
* A weak mapping of receiver to array of sender connections.
*/
const sendersForReceiver = new WeakMap<any, IConnection[]>();
/**
* A set of connection arrays which are pending cleanup.
*/
const dirtySet = new Set<IConnection[]>();
/**
* A function to schedule an event loop callback.
*/
const schedule = (() => {
const ok = typeof requestAnimationFrame === 'function';
return ok ? requestAnimationFrame : setImmediate;
})();
/**
* Find a connection which matches the given parameters.
*/
function findConnection(
connections: IConnection[],
signal: Signal<any, any>,
slot: Slot<any, any>,
thisArg: any,
): IConnection | undefined {
return find(
connections,
connection =>
connection.signal === signal &&
connection.slot === slot &&
connection.thisArg === thisArg,
);
}
/**
* Invoke a slot with the given parameters.
*
* The connection is assumed to be valid.
*
* Exceptions in the slot will be caught and logged.
*/
function invokeSlot(connection: IConnection, args: any): void {
const { signal, slot, thisArg } = connection;
try {
slot.call(thisArg, signal!.sender, args);
} catch (err: unknown) {
exceptionHandler(err as unknown as Error);
}
}
/**
* Schedule a cleanup of a connection array.
*
* This will add the array to the dirty set and schedule a deferred
* cleanup of the array contents. On cleanup, any connection with a
* `null` signal will be removed from the array.
*/
function scheduleCleanup(array: IConnection[]): void {
if (dirtySet.size === 0) {
schedule(cleanupDirtySet);
}
dirtySet.add(array);
}
/**
* Cleanup the connection lists in the dirty set.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupDirtySet(): void {
dirtySet.forEach(cleanupConnections);
dirtySet.clear();
}
/**
* Cleanup the dirty connections in a connections array.
*
* This will remove any connection with a `null` signal.
*
* This function should only be invoked asynchronously, when the
* stack frame is guaranteed to not be on the path of user code.
*/
function cleanupConnections(connections: IConnection[]): void {
ArrayExt.removeAllWhere(connections, isDeadConnection);
}
/**
* Test whether a connection is dead.
*
* A dead connection has a `null` signal.
*/
function isDeadConnection(connection: IConnection): boolean {
return connection.signal === null;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,507 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* A sizer object for use with the box engine layout functions.
*
* #### Notes
* A box sizer holds the geometry information for an object along an
* arbitrary layout orientation.
*
* For best performance, this class should be treated as a raw data
* struct. It should not typically be subclassed.
*/
export class BoxSizer {
/**
* The preferred size for the sizer.
*
* #### Notes
* The sizer will be given this initial size subject to its size
* bounds. The sizer will not deviate from this size unless such
* deviation is required to fit into the available layout space.
*
* There is no limit to this value, but it will be clamped to the
* bounds defined by {@link minSize} and {@link maxSize}.
*
* The default value is `0`.
*/
sizeHint = 0;
/**
* The minimum size of the sizer.
*
* #### Notes
* The sizer will never be sized less than this value, even if
* it means the sizer will overflow the available layout space.
*
* It is assumed that this value lies in the range `[0, Infinity)`
* and that it is `<=` to {@link maxSize}. Failure to adhere to this
* constraint will yield undefined results.
*
* The default value is `0`.
*/
minSize = 0;
/**
* The maximum size of the sizer.
*
* #### Notes
* The sizer will never be sized greater than this value, even if
* it means the sizer will underflow the available layout space.
*
* It is assumed that this value lies in the range `[0, Infinity]`
* and that it is `>=` to {@link minSize}. Failure to adhere to this
* constraint will yield undefined results.
*
* The default value is `Infinity`.
*/
maxSize = Infinity;
/**
* The stretch factor for the sizer.
*
* #### Notes
* This controls how much the sizer stretches relative to its sibling
* sizers when layout space is distributed. A stretch factor of zero
* is special and will cause the sizer to only be resized after all
* other sizers with a stretch factor greater than zero have been
* resized to their limits.
*
* It is assumed that this value is an integer that lies in the range
* `[0, Infinity)`. Failure to adhere to this constraint will yield
* undefined results.
*
* The default value is `1`.
*/
stretch = 1;
/**
* The computed size of the sizer.
*
* #### Notes
* This value is the output of a call to {@link BoxEngine.calc}. It represents
* the computed size for the object along the layout orientation,
* and will always lie in the range `[minSize, maxSize]`.
*
* This value is output only.
*
* Changing this value will have no effect.
*/
size = 0;
/**
* An internal storage property for the layout algorithm.
*
* #### Notes
* This value is used as temporary storage by the layout algorithm.
*
* Changing this value will have no effect.
*/
done = false;
}
/**
* The namespace for the box engine layout functions.
*/
export namespace BoxEngine {
/**
* Calculate the optimal layout sizes for a sequence of box sizers.
*
* This distributes the available layout space among the box sizers
* according to the following algorithm:
*
* 1. Initialize the sizers's size to its size hint and compute the
* sums for each of size hint, min size, and max size.
*
* 2. If the total size hint equals the available space, return.
*
* 3. If the available space is less than the total min size, set all
* sizers to their min size and return.
*
* 4. If the available space is greater than the total max size, set
* all sizers to their max size and return.
*
* 5. If the layout space is less than the total size hint, distribute
* the negative delta as follows:
*
* a. Shrink each sizer with a stretch factor greater than zero by
* an amount proportional to the negative space and the sum of
* stretch factors. If the sizer reaches its min size, remove
* it and its stretch factor from the computation.
*
* b. If after adjusting all stretch sizers there remains negative
* space, distribute the space equally among the sizers with a
* stretch factor of zero. If a sizer reaches its min size,
* remove it from the computation.
*
* 6. If the layout space is greater than the total size hint,
* distribute the positive delta as follows:
*
* a. Expand each sizer with a stretch factor greater than zero by
* an amount proportional to the postive space and the sum of
* stretch factors. If the sizer reaches its max size, remove
* it and its stretch factor from the computation.
*
* b. If after adjusting all stretch sizers there remains positive
* space, distribute the space equally among the sizers with a
* stretch factor of zero. If a sizer reaches its max size,
* remove it from the computation.
*
* 7. return
*
* @param sizers - The sizers for a particular layout line.
*
* @param space - The available layout space for the sizers.
*
* @returns The delta between the provided available space and the
* actual consumed space. This value will be zero if the sizers
* can be adjusted to fit, negative if the available space is too
* small, and positive if the available space is too large.
*
* #### Notes
* The {@link BoxSizer.size} of each sizer is updated with the computed size.
*
* This function can be called at any time to recompute the layout for
* an existing sequence of sizers. The previously computed results will
* have no effect on the new output. It is therefore not necessary to
* create new sizer objects on each resize event.
*/
export function calc(sizers: ArrayLike<BoxSizer>, space: number): number {
// Bail early if there is nothing to do.
const count = sizers.length;
if (count === 0) {
return space;
}
// Setup the size and stretch counters.
let totalMin = 0;
let totalMax = 0;
let totalSize = 0;
let totalStretch = 0;
let stretchCount = 0;
// Setup the sizers and compute the totals.
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
const min = sizer.minSize;
const max = sizer.maxSize;
const hint = sizer.sizeHint;
sizer.done = false;
sizer.size = Math.max(min, Math.min(hint, max));
totalSize += sizer.size;
totalMin += min;
totalMax += max;
if (sizer.stretch > 0) {
totalStretch += sizer.stretch;
stretchCount++;
}
}
// If the space is equal to the total size, return early.
if (space === totalSize) {
return 0;
}
// If the space is less than the total min, minimize each sizer.
if (space <= totalMin) {
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
sizer.size = sizer.minSize;
}
return space - totalMin;
}
// If the space is greater than the total max, maximize each sizer.
if (space >= totalMax) {
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
sizer.size = sizer.maxSize;
}
return space - totalMax;
}
// The loops below perform sub-pixel precision sizing. A near zero
// value is used for compares instead of zero to ensure that the
// loop terminates when the subdivided space is reasonably small.
const nearZero = 0.01;
// A counter which is decremented each time a sizer is resized to
// its limit. This ensures the loops terminate even if there is
// space remaining to distribute.
let notDoneCount = count;
// Distribute negative delta space.
if (space < totalSize) {
// Shrink each stretchable sizer by an amount proportional to its
// stretch factor. If a sizer reaches its min size it's marked as
// done. The loop progresses in phases where each sizer is given
// a chance to consume its fair share for the pass, regardless of
// whether a sizer before it reached its limit. This continues
// until the stretchable sizers or the free space is exhausted.
let freeSpace = totalSize - space;
while (stretchCount > 0 && freeSpace > nearZero) {
const distSpace = freeSpace;
const distStretch = totalStretch;
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
if (sizer.done || sizer.stretch === 0) {
continue;
}
const amt = (sizer.stretch * distSpace) / distStretch;
if (sizer.size - amt <= sizer.minSize) {
freeSpace -= sizer.size - sizer.minSize;
totalStretch -= sizer.stretch;
sizer.size = sizer.minSize;
sizer.done = true;
notDoneCount--;
stretchCount--;
} else {
freeSpace -= amt;
sizer.size -= amt;
}
}
}
// Distribute any remaining space evenly among the non-stretchable
// sizers. This progresses in phases in the same manner as above.
while (notDoneCount > 0 && freeSpace > nearZero) {
const amt = freeSpace / notDoneCount;
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
if (sizer.done) {
continue;
}
if (sizer.size - amt <= sizer.minSize) {
freeSpace -= sizer.size - sizer.minSize;
sizer.size = sizer.minSize;
sizer.done = true;
notDoneCount--;
} else {
freeSpace -= amt;
sizer.size -= amt;
}
}
}
}
// Distribute positive delta space.
else {
// Expand each stretchable sizer by an amount proportional to its
// stretch factor. If a sizer reaches its max size it's marked as
// done. The loop progresses in phases where each sizer is given
// a chance to consume its fair share for the pass, regardless of
// whether a sizer before it reached its limit. This continues
// until the stretchable sizers or the free space is exhausted.
let freeSpace = space - totalSize;
while (stretchCount > 0 && freeSpace > nearZero) {
const distSpace = freeSpace;
const distStretch = totalStretch;
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
if (sizer.done || sizer.stretch === 0) {
continue;
}
const amt = (sizer.stretch * distSpace) / distStretch;
if (sizer.size + amt >= sizer.maxSize) {
freeSpace -= sizer.maxSize - sizer.size;
totalStretch -= sizer.stretch;
sizer.size = sizer.maxSize;
sizer.done = true;
notDoneCount--;
stretchCount--;
} else {
freeSpace -= amt;
sizer.size += amt;
}
}
}
// Distribute any remaining space evenly among the non-stretchable
// sizers. This progresses in phases in the same manner as above.
while (notDoneCount > 0 && freeSpace > nearZero) {
const amt = freeSpace / notDoneCount;
for (let i = 0; i < count; ++i) {
const sizer = sizers[i];
if (sizer.done) {
continue;
}
if (sizer.size + amt >= sizer.maxSize) {
freeSpace -= sizer.maxSize - sizer.size;
sizer.size = sizer.maxSize;
sizer.done = true;
notDoneCount--;
} else {
freeSpace -= amt;
sizer.size += amt;
}
}
}
}
// Indicate that the consumed space equals the available space.
return 0;
}
/**
* Adjust a sizer by a delta and update its neighbors accordingly.
*
* @param sizers - The sizers which should be adjusted.
*
* @param index - The index of the sizer to grow.
*
* @param delta - The amount to adjust the sizer, positive or negative.
*
* #### Notes
* This will adjust the indicated sizer by the specified amount, along
* with the sizes of the appropriate neighbors, subject to the limits
* specified by each of the sizers.
*
* This is useful when implementing box layouts where the boundaries
* between the sizers are interactively adjustable by the user.
*/
export function adjust(
sizers: ArrayLike<BoxSizer>,
index: number,
delta: number,
): void {
// Bail early when there is nothing to do.
if (sizers.length === 0 || delta === 0) {
return;
}
// Dispatch to the proper implementation.
if (delta > 0) {
growSizer(sizers, index, delta);
} else {
shrinkSizer(sizers, index, -delta);
}
}
/**
* Grow a sizer by a positive delta and adjust neighbors.
*/
function growSizer(
sizers: ArrayLike<BoxSizer>,
index: number,
delta: number,
): void {
// Compute how much the items to the left can expand.
let growLimit = 0;
for (let i = 0; i <= index; ++i) {
const sizer = sizers[i];
growLimit += sizer.maxSize - sizer.size;
}
// Compute how much the items to the right can shrink.
let shrinkLimit = 0;
for (let i = index + 1, n = sizers.length; i < n; ++i) {
const sizer = sizers[i];
shrinkLimit += sizer.size - sizer.minSize;
}
// Clamp the delta adjustment to the limits.
delta = Math.min(delta, growLimit, shrinkLimit);
// Grow the sizers to the left by the delta.
let grow = delta;
for (let i = index; i >= 0 && grow > 0; --i) {
const sizer = sizers[i];
const limit = sizer.maxSize - sizer.size;
if (limit >= grow) {
sizer.sizeHint = sizer.size + grow;
grow = 0;
} else {
sizer.sizeHint = sizer.size + limit;
grow -= limit;
}
}
// Shrink the sizers to the right by the delta.
let shrink = delta;
for (let i = index + 1, n = sizers.length; i < n && shrink > 0; ++i) {
const sizer = sizers[i];
const limit = sizer.size - sizer.minSize;
if (limit >= shrink) {
sizer.sizeHint = sizer.size - shrink;
shrink = 0;
} else {
sizer.sizeHint = sizer.size - limit;
shrink -= limit;
}
}
}
/**
* Shrink a sizer by a positive delta and adjust neighbors.
*/
function shrinkSizer(
sizers: ArrayLike<BoxSizer>,
index: number,
delta: number,
): void {
// Compute how much the items to the right can expand.
let growLimit = 0;
for (let i = index + 1, n = sizers.length; i < n; ++i) {
const sizer = sizers[i];
growLimit += sizer.maxSize - sizer.size;
}
// Compute how much the items to the left can shrink.
let shrinkLimit = 0;
for (let i = 0; i <= index; ++i) {
const sizer = sizers[i];
shrinkLimit += sizer.size - sizer.minSize;
}
// Clamp the delta adjustment to the limits.
delta = Math.min(delta, growLimit, shrinkLimit);
// Grow the sizers to the right by the delta.
let grow = delta;
for (let i = index + 1, n = sizers.length; i < n && grow > 0; ++i) {
const sizer = sizers[i];
const limit = sizer.maxSize - sizer.size;
if (limit >= grow) {
sizer.sizeHint = sizer.size + grow;
grow = 0;
} else {
sizer.sizeHint = sizer.size + limit;
grow -= limit;
}
}
// Shrink the sizers to the left by the delta.
let shrink = delta;
for (let i = index; i >= 0 && shrink > 0; --i) {
const sizer = sizers[i];
const limit = sizer.size - sizer.minSize;
if (limit >= shrink) {
sizer.sizeHint = sizer.size - shrink;
shrink = 0;
} else {
sizer.sizeHint = sizer.size - limit;
shrink -= limit;
}
}
}
}

View File

@@ -0,0 +1,671 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { AttachedProperty } from '../properties';
import { type Message, MessageLoop } from '../messaging';
import { ElementExt } from '../domutils';
import { ArrayExt } from '../algorithm';
import { Widget } from './widget';
import Utils from './utils';
import { PanelLayout } from './panellayout';
import { LayoutItem } from './layout';
import { BoxEngine, BoxSizer } from './boxengine';
/**
* A layout which arranges its widgets in a single row or column.
*/
export class BoxLayout extends PanelLayout {
/**
* Construct a new box layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options: BoxLayout.IOptions = {}) {
super();
if (options.direction !== undefined) {
this._direction = options.direction;
}
if (options.alignment !== undefined) {
this._alignment = options.alignment;
}
if (options.spacing !== undefined) {
this._spacing = Utils.clampDimension(options.spacing);
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose(): void {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._sizers.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Get the layout direction for the box layout.
*/
get direction(): BoxLayout.Direction {
return this._direction;
}
/**
* Set the layout direction for the box layout.
*/
set direction(value: BoxLayout.Direction) {
if (this._direction === value) {
return;
}
this._direction = value;
if (!this.parent) {
return;
}
this.parent.dataset.direction = value;
this.parent.fit();
}
/**
* Get the content alignment for the box layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
get alignment(): BoxLayout.Alignment {
return this._alignment;
}
/**
* Set the content alignment for the box layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
set alignment(value: BoxLayout.Alignment) {
if (this._alignment === value) {
return;
}
this._alignment = value;
if (!this.parent) {
return;
}
this.parent.dataset.alignment = value;
this.parent.update();
}
/**
* Get the inter-element spacing for the box layout.
*/
get spacing(): number {
return this._spacing;
}
/**
* Set the inter-element spacing for the box layout.
*/
set spacing(value: number) {
value = Utils.clampDimension(value);
if (this._spacing === value) {
return;
}
this._spacing = value;
if (!this.parent) {
return;
}
this.parent.fit();
}
/**
* Perform layout initialization which requires the parent widget.
*/
protected init(): void {
this.parent!.dataset.direction = this.direction;
this.parent!.dataset.alignment = this.alignment;
super.init();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected attachWidget(index: number, widget: Widget): void {
// Create and add a new layout item for the widget.
ArrayExt.insert(this._items, index, new LayoutItem(widget));
// Create and add a new sizer for the widget.
ArrayExt.insert(this._sizers, index, new BoxSizer());
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent!.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected moveWidget(
fromIndex: number,
toIndex: number,
widget: Widget,
): void {
// Move the layout item for the widget.
ArrayExt.move(this._items, fromIndex, toIndex);
// Move the sizer for the widget.
ArrayExt.move(this._sizers, fromIndex, toIndex);
// Post an update request for the parent widget.
this.parent!.update();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected detachWidget(index: number, widget: Widget): void {
// Remove the layout item for the widget.
const item = ArrayExt.removeAt(this._items, index);
// Remove the sizer for the widget.
ArrayExt.removeAt(this._sizers, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Dispose of the layout item.
item!.dispose();
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
protected onBeforeShow(msg: Message): void {
super.onBeforeShow(msg);
this.parent!.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
protected onBeforeAttach(msg: Message): void {
super.onBeforeAttach(msg);
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
protected onChildShown(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
protected onResize(msg: Widget.ResizeMessage): void {
if (this.parent!.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
protected onUpdateRequest(msg: Message): void {
if (this.parent!.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
protected onFitRequest(msg: Message): void {
if (this.parent!.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
private _fit(): void {
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Update the fixed space for the visible items.
this._fixed = this._spacing * Math.max(0, nVisible - 1);
// Setup the computed minimum size.
const horz = Private.isHorizontal(this._direction);
let minW = horz ? this._fixed : 0;
let minH = horz ? 0 : this._fixed;
// Update the sizers and computed minimum size.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item and corresponding box sizer.
const item = this._items[i];
const sizer = this._sizers[i];
// If the item is hidden, it should consume zero size.
if (item.isHidden) {
sizer.minSize = 0;
sizer.maxSize = 0;
continue;
}
// Update the size limits for the item.
item.fit();
// Update the size basis and stretch factor.
sizer.sizeHint = BoxLayout.getSizeBasis(item.widget);
sizer.stretch = BoxLayout.getStretch(item.widget);
// Update the sizer limits and computed min size.
if (horz) {
sizer.minSize = item.minWidth;
sizer.maxSize = item.maxWidth;
minW += item.minWidth;
minH = Math.max(minH, item.minHeight);
} else {
sizer.minSize = item.minHeight;
sizer.maxSize = item.maxHeight;
minH += item.minHeight;
minW = Math.max(minW, item.minWidth);
}
}
// Update the box sizing and add it to the computed min size.
const box = (this._box = ElementExt.boxSizing(this.parent!.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
const { style } = this.parent!.node;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent!.parent) {
MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
private _update(offsetWidth: number, offsetHeight: number): void {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent!.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent!.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = ElementExt.boxSizing(this.parent!.node);
}
// Compute the layout area adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
const width = offsetWidth - this._box.horizontalSum;
const height = offsetHeight - this._box.verticalSum;
// Distribute the layout space and adjust the start position.
let delta: number;
switch (this._direction) {
case 'left-to-right':
delta = BoxEngine.calc(this._sizers, Math.max(0, width - this._fixed));
break;
case 'top-to-bottom':
delta = BoxEngine.calc(this._sizers, Math.max(0, height - this._fixed));
break;
case 'right-to-left':
delta = BoxEngine.calc(this._sizers, Math.max(0, width - this._fixed));
left += width;
break;
case 'bottom-to-top':
delta = BoxEngine.calc(this._sizers, Math.max(0, height - this._fixed));
top += height;
break;
default:
throw 'unreachable';
}
// Setup the variables for justification and alignment offset.
let extra = 0;
let offset = 0;
// Account for alignment if there is extra layout space.
if (delta > 0) {
switch (this._alignment) {
case 'start':
break;
case 'center':
extra = 0;
offset = delta / 2;
break;
case 'end':
extra = 0;
offset = delta;
break;
case 'justify':
extra = delta / nVisible;
offset = 0;
break;
default:
throw 'unreachable';
}
}
// Layout the items using the computed box sizes.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
const item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Fetch the computed size for the widget.
const { size } = this._sizers[i];
// Update the widget geometry and advance the relevant edge.
switch (this._direction) {
case 'left-to-right':
item.update(left + offset, top, size + extra, height);
left += size + extra + this._spacing;
break;
case 'top-to-bottom':
item.update(left, top + offset, width, size + extra);
top += size + extra + this._spacing;
break;
case 'right-to-left':
item.update(left - offset - size - extra, top, size + extra, height);
left -= size + extra + this._spacing;
break;
case 'bottom-to-top':
item.update(left, top - offset - size - extra, width, size + extra);
top -= size + extra + this._spacing;
break;
default:
throw 'unreachable';
}
}
}
private _fixed = 0;
private _spacing = 4;
private _dirty = false;
private _sizers: BoxSizer[] = [];
private _items: LayoutItem[] = [];
private _box: ElementExt.IBoxSizing | null = null;
private _alignment: BoxLayout.Alignment = 'start';
private _direction: BoxLayout.Direction = 'top-to-bottom';
}
/**
* The namespace for the `BoxLayout` class statics.
*/
export namespace BoxLayout {
/**
* A type alias for a box layout direction.
*/
export type Direction =
| 'left-to-right'
| 'right-to-left'
| 'top-to-bottom'
| 'bottom-to-top';
/**
* A type alias for a box layout alignment.
*/
export type Alignment = 'start' | 'center' | 'end' | 'justify';
/**
* An options object for initializing a box layout.
*/
export interface IOptions {
/**
* The direction of the layout.
*
* The default is `'top-to-bottom'`.
*/
direction?: Direction;
/**
* The content alignment of the layout.
*
* The default is `'start'`.
*/
alignment?: Alignment;
/**
* The spacing between items in the layout.
*
* The default is `4`.
*/
spacing?: number;
}
/**
* Get the box layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box layout stretch factor for the widget.
*/
export function getStretch(widget: Widget): number {
return Private.stretchProperty.get(widget);
}
/**
* Set the box layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
export function setStretch(widget: Widget, value: number): void {
Private.stretchProperty.set(widget, value);
}
/**
* Get the box layout size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box layout size basis for the widget.
*/
export function getSizeBasis(widget: Widget): number {
return Private.sizeBasisProperty.get(widget);
}
/**
* Set the box layout size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the size basis.
*/
export function setSizeBasis(widget: Widget, value: number): void {
Private.sizeBasisProperty.set(widget, value);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* The property descriptor for a widget stretch factor.
*/
export const stretchProperty = new AttachedProperty<Widget, number>({
name: 'stretch',
create: () => 0,
coerce: (owner, value) => Math.max(0, Math.floor(value)),
changed: onChildSizingChanged,
});
/**
* The property descriptor for a widget size basis.
*/
export const sizeBasisProperty = new AttachedProperty<Widget, number>({
name: 'sizeBasis',
create: () => 0,
coerce: (owner, value) => Math.max(0, Math.floor(value)),
changed: onChildSizingChanged,
});
/**
* Test whether a direction has horizontal orientation.
*/
export function isHorizontal(dir: BoxLayout.Direction): boolean {
return dir === 'left-to-right' || dir === 'right-to-left';
}
/**
* Clamp a spacing value to an integer >= 0.
*/
export function clampSpacing(value: number): number {
return Math.max(0, Math.floor(value));
}
/**
* The change handler for the attached sizing properties.
*/
function onChildSizingChanged(child: Widget): void {
if (child.parent && child.parent.layout instanceof BoxLayout) {
child.parent.fit();
}
}
}

View File

@@ -0,0 +1,220 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type Widget } from './widget';
import { Panel } from './panel';
import { BoxLayout } from './boxlayout';
/**
* A panel which arranges its widgets in a single row or column.
*
* #### Notes
* This class provides a convenience wrapper around a {@link BoxLayout}.
*/
export class BoxPanel extends Panel {
/**
* Construct a new box panel.
*
* @param options - The options for initializing the box panel.
*/
constructor(options: BoxPanel.IOptions = {}) {
super({ layout: Private.createLayout(options) });
this.addClass('lm-BoxPanel');
}
/**
* Get the layout direction for the box panel.
*/
get direction(): BoxPanel.Direction {
return (this.layout as BoxLayout).direction;
}
/**
* Set the layout direction for the box panel.
*/
set direction(value: BoxPanel.Direction) {
(this.layout as BoxLayout).direction = value;
}
/**
* Get the content alignment for the box panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
get alignment(): BoxPanel.Alignment {
return (this.layout as BoxLayout).alignment;
}
/**
* Set the content alignment for the box panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire box layout.
*/
set alignment(value: BoxPanel.Alignment) {
(this.layout as BoxLayout).alignment = value;
}
/**
* Get the inter-element spacing for the box panel.
*/
get spacing(): number {
return (this.layout as BoxLayout).spacing;
}
/**
* Set the inter-element spacing for the box panel.
*/
set spacing(value: number) {
(this.layout as BoxLayout).spacing = value;
}
/**
* A message handler invoked on a `'child-added'` message.
*/
protected onChildAdded(msg: Widget.ChildMessage): void {
msg.child.addClass('lm-BoxPanel-child');
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
protected onChildRemoved(msg: Widget.ChildMessage): void {
msg.child.removeClass('lm-BoxPanel-child');
}
}
/**
* The namespace for the `BoxPanel` class statics.
*/
export namespace BoxPanel {
/**
* A type alias for a box panel direction.
*/
export type Direction = BoxLayout.Direction;
/**
* A type alias for a box panel alignment.
*/
export type Alignment = BoxLayout.Alignment;
/**
* An options object for initializing a box panel.
*/
export interface IOptions {
/**
* The layout direction of the panel.
*
* The default is `'top-to-bottom'`.
*/
direction?: Direction;
/**
* The content alignment of the panel.
*
* The default is `'start'`.
*/
alignment?: Alignment;
/**
* The spacing between items in the panel.
*
* The default is `4`.
*/
spacing?: number;
/**
* The box layout to use for the box panel.
*
* If this is provided, the other options are ignored.
*
* The default is a new `BoxLayout`.
*/
layout?: BoxLayout;
}
/**
* Get the box panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box panel stretch factor for the widget.
*/
export function getStretch(widget: Widget): number {
return BoxLayout.getStretch(widget);
}
/**
* Set the box panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
export function setStretch(widget: Widget, value: number): void {
BoxLayout.setStretch(widget, value);
}
/**
* Get the box panel size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The box panel size basis for the widget.
*/
export function getSizeBasis(widget: Widget): number {
return BoxLayout.getSizeBasis(widget);
}
/**
* Set the box panel size basis for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the size basis.
*/
export function setSizeBasis(widget: Widget, value: number): void {
BoxLayout.setSizeBasis(widget, value);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* Create a box layout for the given panel options.
*/
export function createLayout(options: BoxPanel.IOptions): BoxLayout {
return options.layout || new BoxLayout(options);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,390 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type ISignal, Signal } from '../signaling';
import { type IDisposable } from '../disposable';
import { ArrayExt, find, max } from '../algorithm';
import { type Widget } from './widget';
/**
* A class which tracks focus among a set of widgets.
*
* This class is useful when code needs to keep track of the most
* recently focused widget(s) among a set of related widgets.
*/
export class FocusTracker<T extends Widget> implements IDisposable {
/**
* Dispose of the resources held by the tracker.
*/
dispose(): void {
// Do nothing if the tracker is already disposed.
if (this._counter < 0) {
return;
}
// Mark the tracker as disposed.
this._counter = -1;
// Clear the connections for the tracker.
Signal.clearData(this);
// Remove all event listeners.
for (const widget of this._widgets) {
widget.node.removeEventListener('focus', this, true);
widget.node.removeEventListener('blur', this, true);
}
// Clear the internal data structures.
this._activeWidget = null;
this._currentWidget = null;
this._nodes.clear();
this._numbers.clear();
this._widgets.length = 0;
}
/**
* A signal emitted when the current widget has changed.
*/
get currentChanged(): ISignal<this, FocusTracker.IChangedArgs<T>> {
return this._currentChanged;
}
/**
* A signal emitted when the active widget has changed.
*/
get activeChanged(): ISignal<this, FocusTracker.IChangedArgs<T>> {
return this._activeChanged;
}
/**
* A flag indicating whether the tracker is disposed.
*/
get isDisposed(): boolean {
return this._counter < 0;
}
/**
* The current widget in the tracker.
*
* #### Notes
* The current widget is the widget among the tracked widgets which
* has the *descendant node* which has most recently been focused.
*
* The current widget will not be updated if the node loses focus. It
* will only be updated when a different tracked widget gains focus.
*
* If the current widget is removed from the tracker, the previous
* current widget will be restored.
*
* This behavior is intended to follow a user's conceptual model of
* a semantically "current" widget, where the "last thing of type X"
* to be interacted with is the "current instance of X", regardless
* of whether that instance still has focus.
*/
get currentWidget(): T | null {
return this._currentWidget;
}
/**
* The active widget in the tracker.
*
* #### Notes
* The active widget is the widget among the tracked widgets which
* has the *descendant node* which is currently focused.
*/
get activeWidget(): T | null {
return this._activeWidget;
}
/**
* A read only array of the widgets being tracked.
*/
get widgets(): ReadonlyArray<T> {
return this._widgets;
}
/**
* Get the focus number for a particular widget in the tracker.
*
* @param widget - The widget of interest.
*
* @returns The focus number for the given widget, or `-1` if the
* widget has not had focus since being added to the tracker, or
* is not contained by the tracker.
*
* #### Notes
* The focus number indicates the relative order in which the widgets
* have gained focus. A widget with a larger number has gained focus
* more recently than a widget with a smaller number.
*
* The `currentWidget` will always have the largest focus number.
*
* All widgets start with a focus number of `-1`, which indicates that
* the widget has not been focused since being added to the tracker.
*/
focusNumber(widget: T): number {
const n = this._numbers.get(widget);
return n === undefined ? -1 : n;
}
/**
* Test whether the focus tracker contains a given widget.
*
* @param widget - The widget of interest.
*
* @returns `true` if the widget is tracked, `false` otherwise.
*/
has(widget: T): boolean {
return this._numbers.has(widget);
}
/**
* Add a widget to the focus tracker.
*
* @param widget - The widget of interest.
*
* #### Notes
* A widget will be automatically removed from the tracker if it
* is disposed after being added.
*
* If the widget is already tracked, this is a no-op.
*/
add(widget: T): void {
// Do nothing if the widget is already tracked.
if (this._numbers.has(widget)) {
return;
}
// Test whether the widget has focus.
const focused = widget.node.contains(document.activeElement);
// Set up the initial focus number.
const n = focused ? this._counter++ : -1;
// Add the widget to the internal data structures.
this._widgets.push(widget);
this._numbers.set(widget, n);
this._nodes.set(widget.node, widget);
// Set up the event listeners. The capturing phase must be used
// since the 'focus' and 'blur' events don't bubble and Firefox
// doesn't support the 'focusin' or 'focusout' events.
widget.node.addEventListener('focus', this, true);
widget.node.addEventListener('blur', this, true);
// Connect the disposed signal handler.
widget.disposed.connect(this._onWidgetDisposed, this);
// Set the current and active widgets if needed.
if (focused) {
this._setWidgets(widget, widget);
}
}
/**
* Remove a widget from the focus tracker.
*
* #### Notes
* If the widget is the `currentWidget`, the previous current widget
* will become the new `currentWidget`.
*
* A widget will be automatically removed from the tracker if it
* is disposed after being added.
*
* If the widget is not tracked, this is a no-op.
*/
remove(widget: T): void {
// Bail early if the widget is not tracked.
if (!this._numbers.has(widget)) {
return;
}
// Disconnect the disposed signal handler.
widget.disposed.disconnect(this._onWidgetDisposed, this);
// Remove the event listeners.
widget.node.removeEventListener('focus', this, true);
widget.node.removeEventListener('blur', this, true);
// Remove the widget from the internal data structures.
ArrayExt.removeFirstOf(this._widgets, widget);
this._nodes.delete(widget.node);
this._numbers.delete(widget);
// Bail early if the widget is not the current widget.
if (this._currentWidget !== widget) {
return;
}
// Filter the widgets for those which have had focus.
const valid = this._widgets.filter(w => this._numbers.get(w) !== -1);
// Get the valid widget with the max focus number.
const previous =
max(valid, (first, second) => {
const a = this._numbers.get(first)!;
const b = this._numbers.get(second)!;
return a - b;
}) || null;
// Set the current and active widgets.
this._setWidgets(previous, null);
}
/**
* Handle the DOM events for the focus tracker.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the tracked nodes. It should
* not be called directly by user code.
*/
handleEvent(event: Event): void {
switch (event.type) {
case 'focus':
this._evtFocus(event as FocusEvent);
break;
case 'blur':
this._evtBlur(event as FocusEvent);
break;
default:
break;
}
}
/**
* Set the current and active widgets for the tracker.
*/
private _setWidgets(current: T | null, active: T | null): void {
// Swap the current widget.
const oldCurrent = this._currentWidget;
this._currentWidget = current;
// Swap the active widget.
const oldActive = this._activeWidget;
this._activeWidget = active;
// Emit the `currentChanged` signal if needed.
if (oldCurrent !== current) {
this._currentChanged.emit({ oldValue: oldCurrent, newValue: current });
}
// Emit the `activeChanged` signal if needed.
if (oldActive !== active) {
this._activeChanged.emit({ oldValue: oldActive, newValue: active });
}
}
/**
* Handle the `'focus'` event for a tracked widget.
*/
private _evtFocus(event: FocusEvent): void {
// Find the widget which gained focus, which is known to exist.
const widget = this._nodes.get(event.currentTarget as HTMLElement)!;
// Update the focus number if necessary.
if (widget !== this._currentWidget) {
this._numbers.set(widget, this._counter++);
}
// Set the current and active widgets.
this._setWidgets(widget, widget);
}
/**
* Handle the `'blur'` event for a tracked widget.
*/
private _evtBlur(event: FocusEvent): void {
// Find the widget which lost focus, which is known to exist.
const widget = this._nodes.get(event.currentTarget as HTMLElement)!;
// Get the node which being focused after this blur.
const focusTarget = event.relatedTarget as HTMLElement;
// If no other node is being focused, clear the active widget.
if (!focusTarget) {
this._setWidgets(this._currentWidget, null);
return;
}
// Bail if the focus widget is not changing.
if (widget.node.contains(focusTarget)) {
return;
}
// If no tracked widget is being focused, clear the active widget.
if (!find(this._widgets, w => w.node.contains(focusTarget))) {
this._setWidgets(this._currentWidget, null);
return;
}
}
/**
* Handle the `disposed` signal for a tracked widget.
*/
private _onWidgetDisposed(sender: T): void {
this.remove(sender);
}
private _counter = 0;
private _widgets: T[] = [];
private _activeWidget: T | null = null;
private _currentWidget: T | null = null;
private _numbers = new Map<T, number>();
private _nodes = new Map<HTMLElement, T>();
private _activeChanged = new Signal<this, FocusTracker.IChangedArgs<T>>(this);
private _currentChanged = new Signal<this, FocusTracker.IChangedArgs<T>>(
this,
);
}
/**
* The namespace for the `FocusTracker` class statics.
*/
export namespace FocusTracker {
/**
* An arguments object for the changed signals.
*/
export interface IChangedArgs<T extends Widget> {
/**
* The old value for the widget.
*/
oldValue: T | null;
/**
* The new value for the widget.
*/
newValue: T | null;
}
}

View File

@@ -0,0 +1,890 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { AttachedProperty } from '../properties';
import { type Message, MessageLoop } from '../messaging';
import { ElementExt } from '../domutils';
import { ArrayExt } from '../algorithm';
import { Widget } from './widget';
import { Layout, LayoutItem } from './layout';
import { BoxEngine, BoxSizer } from './boxengine';
/**
* A layout which arranges its widgets in a grid.
*/
export class GridLayout extends Layout {
/**
* Construct a new grid layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options: GridLayout.IOptions = {}) {
super(options);
if (options.rowCount !== undefined) {
Private.reallocSizers(this._rowSizers, options.rowCount);
}
if (options.columnCount !== undefined) {
Private.reallocSizers(this._columnSizers, options.columnCount);
}
if (options.rowSpacing !== undefined) {
this._rowSpacing = Private.clampValue(options.rowSpacing);
}
if (options.columnSpacing !== undefined) {
this._columnSpacing = Private.clampValue(options.columnSpacing);
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose(): void {
// Dispose of the widgets and layout items.
for (const item of this._items) {
const { widget } = item;
item.dispose();
widget.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._rowStarts.length = 0;
this._rowSizers.length = 0;
this._columnStarts.length = 0;
this._columnSizers.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Get the number of rows in the layout.
*/
get rowCount(): number {
return this._rowSizers.length;
}
/**
* Set the number of rows in the layout.
*
* #### Notes
* The minimum row count is `1`.
*/
set rowCount(value: number) {
// Do nothing if the row count does not change.
if (value === this.rowCount) {
return;
}
// Reallocate the row sizers.
Private.reallocSizers(this._rowSizers, value);
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the number of columns in the layout.
*/
get columnCount(): number {
return this._columnSizers.length;
}
/**
* Set the number of columns in the layout.
*
* #### Notes
* The minimum column count is `1`.
*/
set columnCount(value: number) {
// Do nothing if the column count does not change.
if (value === this.columnCount) {
return;
}
// Reallocate the column sizers.
Private.reallocSizers(this._columnSizers, value);
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the row spacing for the layout.
*/
get rowSpacing(): number {
return this._rowSpacing;
}
/**
* Set the row spacing for the layout.
*/
set rowSpacing(value: number) {
// Clamp the spacing to the allowed range.
value = Private.clampValue(value);
// Bail if the spacing does not change
if (this._rowSpacing === value) {
return;
}
// Update the internal spacing.
this._rowSpacing = value;
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the column spacing for the layout.
*/
get columnSpacing(): number {
return this._columnSpacing;
}
/**
* Set the col spacing for the layout.
*/
set columnSpacing(value: number) {
// Clamp the spacing to the allowed range.
value = Private.clampValue(value);
// Bail if the spacing does not change
if (this._columnSpacing === value) {
return;
}
// Update the internal spacing.
this._columnSpacing = value;
// Schedule a fit of the parent.
if (this.parent) {
this.parent.fit();
}
}
/**
* Get the stretch factor for a specific row.
*
* @param index - The row index of interest.
*
* @returns The stretch factor for the row.
*
* #### Notes
* This returns `-1` if the index is out of range.
*/
rowStretch(index: number): number {
const sizer = this._rowSizers[index];
return sizer ? sizer.stretch : -1;
}
/**
* Set the stretch factor for a specific row.
*
* @param index - The row index of interest.
*
* @param value - The stretch factor for the row.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
setRowStretch(index: number, value: number): void {
// Look up the row sizer.
const sizer = this._rowSizers[index];
// Bail if the index is out of range.
if (!sizer) {
return;
}
// Clamp the value to the allowed range.
value = Private.clampValue(value);
// Bail if the stretch does not change.
if (sizer.stretch === value) {
return;
}
// Update the sizer stretch.
sizer.stretch = value;
// Schedule an update of the parent.
if (this.parent) {
this.parent.update();
}
}
/**
* Get the stretch factor for a specific column.
*
* @param index - The column index of interest.
*
* @returns The stretch factor for the column.
*
* #### Notes
* This returns `-1` if the index is out of range.
*/
columnStretch(index: number): number {
const sizer = this._columnSizers[index];
return sizer ? sizer.stretch : -1;
}
/**
* Set the stretch factor for a specific column.
*
* @param index - The column index of interest.
*
* @param value - The stretch factor for the column.
*
* #### Notes
* This is a no-op if the index is out of range.
*/
setColumnStretch(index: number, value: number): void {
// Look up the column sizer.
const sizer = this._columnSizers[index];
// Bail if the index is out of range.
if (!sizer) {
return;
}
// Clamp the value to the allowed range.
value = Private.clampValue(value);
// Bail if the stretch does not change.
if (sizer.stretch === value) {
return;
}
// Update the sizer stretch.
sizer.stretch = value;
// Schedule an update of the parent.
if (this.parent) {
this.parent.update();
}
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
*[Symbol.iterator](): IterableIterator<Widget> {
for (const item of this._items) {
yield item.widget;
}
}
/**
* Add a widget to the grid layout.
*
* @param widget - The widget to add to the layout.
*
* #### Notes
* If the widget is already contained in the layout, this is no-op.
*/
addWidget(widget: Widget): void {
// Look up the index for the widget.
const i = ArrayExt.findFirstIndex(this._items, it => it.widget === widget);
// Bail if the widget is already in the layout.
if (i !== -1) {
return;
}
// Add the widget to the layout.
this._items.push(new LayoutItem(widget));
// Attach the widget to the parent.
if (this.parent) {
this.attachWidget(widget);
}
}
/**
* Remove a widget from the grid layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget: Widget): void {
// Look up the index for the widget.
const i = ArrayExt.findFirstIndex(this._items, it => it.widget === widget);
// Bail if the widget is not in the layout.
if (i === -1) {
return;
}
// Remove the widget from the layout.
const item = ArrayExt.removeAt(this._items, i)!;
// Detach the widget from the parent.
if (this.parent) {
this.detachWidget(widget);
}
// Dispose the layout item.
item.dispose();
}
/**
* Perform layout initialization which requires the parent widget.
*/
protected init(): void {
super.init();
for (const widget of this) {
this.attachWidget(widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param widget - The widget to attach to the parent.
*/
protected attachWidget(widget: Widget): void {
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent!.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param widget - The widget to detach from the parent.
*/
protected detachWidget(widget: Widget): void {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
protected onBeforeShow(msg: Message): void {
super.onBeforeShow(msg);
this.parent!.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
protected onBeforeAttach(msg: Message): void {
super.onBeforeAttach(msg);
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
protected onChildShown(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
protected onResize(msg: Widget.ResizeMessage): void {
if (this.parent!.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
protected onUpdateRequest(msg: Message): void {
if (this.parent!.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
protected onFitRequest(msg: Message): void {
if (this.parent!.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
private _fit(): void {
// Reset the min sizes of the sizers.
for (let i = 0, n = this.rowCount; i < n; ++i) {
this._rowSizers[i].minSize = 0;
}
for (let i = 0, n = this.columnCount; i < n; ++i) {
this._columnSizers[i].minSize = 0;
}
// Filter for the visible layout items.
const items = this._items.filter(it => !it.isHidden);
// Fit the layout items.
for (let i = 0, n = items.length; i < n; ++i) {
items[i].fit();
}
// Get the max row and column index.
const maxRow = this.rowCount - 1;
const maxCol = this.columnCount - 1;
// Sort the items by row span.
items.sort(Private.rowSpanCmp);
// Update the min sizes of the row sizers.
for (let i = 0, n = items.length; i < n; ++i) {
// Fetch the item.
const item = items[i];
// Get the row bounds for the item.
const config = GridLayout.getCellConfig(item.widget);
const r1 = Math.min(config.row, maxRow);
const r2 = Math.min(config.row + config.rowSpan - 1, maxRow);
// Distribute the minimum height to the sizers as needed.
Private.distributeMin(this._rowSizers, r1, r2, item.minHeight);
}
// Sort the items by column span.
items.sort(Private.columnSpanCmp);
// Update the min sizes of the column sizers.
for (let i = 0, n = items.length; i < n; ++i) {
// Fetch the item.
const item = items[i];
// Get the column bounds for the item.
const config = GridLayout.getCellConfig(item.widget);
const c1 = Math.min(config.column, maxCol);
const c2 = Math.min(config.column + config.columnSpan - 1, maxCol);
// Distribute the minimum width to the sizers as needed.
Private.distributeMin(this._columnSizers, c1, c2, item.minWidth);
}
// If no size constraint is needed, just update the parent.
if (this.fitPolicy === 'set-no-constraint') {
MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
return;
}
// Set up the computed min size.
let minH = maxRow * this._rowSpacing;
let minW = maxCol * this._columnSpacing;
// Add the sizer minimums to the computed min size.
for (let i = 0, n = this.rowCount; i < n; ++i) {
minH += this._rowSizers[i].minSize;
}
for (let i = 0, n = this.columnCount; i < n; ++i) {
minW += this._columnSizers[i].minSize;
}
// Update the box sizing and add it to the computed min size.
const box = (this._box = ElementExt.boxSizing(this.parent!.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
const { style } = this.parent!.node;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent!.parent) {
MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
private _update(offsetWidth: number, offsetHeight: number): void {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent!.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent!.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = ElementExt.boxSizing(this.parent!.node);
}
// Compute the layout area adjusted for border and padding.
const top = this._box.paddingTop;
const left = this._box.paddingLeft;
const width = offsetWidth - this._box.horizontalSum;
const height = offsetHeight - this._box.verticalSum;
// Get the max row and column index.
const maxRow = this.rowCount - 1;
const maxCol = this.columnCount - 1;
// Compute the total fixed row and column space.
const fixedRowSpace = maxRow * this._rowSpacing;
const fixedColSpace = maxCol * this._columnSpacing;
// Distribute the available space to the box sizers.
BoxEngine.calc(this._rowSizers, Math.max(0, height - fixedRowSpace));
BoxEngine.calc(this._columnSizers, Math.max(0, width - fixedColSpace));
// Update the row start positions.
for (let i = 0, pos = top, n = this.rowCount; i < n; ++i) {
this._rowStarts[i] = pos;
pos += this._rowSizers[i].size + this._rowSpacing;
}
// Update the column start positions.
for (let i = 0, pos = left, n = this.columnCount; i < n; ++i) {
this._columnStarts[i] = pos;
pos += this._columnSizers[i].size + this._columnSpacing;
}
// Update the geometry of the layout items.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
const item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Fetch the cell bounds for the widget.
const config = GridLayout.getCellConfig(item.widget);
const r1 = Math.min(config.row, maxRow);
const c1 = Math.min(config.column, maxCol);
const r2 = Math.min(config.row + config.rowSpan - 1, maxRow);
const c2 = Math.min(config.column + config.columnSpan - 1, maxCol);
// Compute the cell geometry.
const x = this._columnStarts[c1];
const y = this._rowStarts[r1];
const w = this._columnStarts[c2] + this._columnSizers[c2].size - x;
const h = this._rowStarts[r2] + this._rowSizers[r2].size - y;
// Update the geometry of the layout item.
item.update(x, y, w, h);
}
}
private _dirty = false;
private _rowSpacing = 4;
private _columnSpacing = 4;
private _items: LayoutItem[] = [];
private _rowStarts: number[] = [];
private _columnStarts: number[] = [];
private _rowSizers: BoxSizer[] = [new BoxSizer()];
private _columnSizers: BoxSizer[] = [new BoxSizer()];
private _box: ElementExt.IBoxSizing | null = null;
}
/**
* The namespace for the `GridLayout` class statics.
*/
export namespace GridLayout {
/**
* An options object for initializing a grid layout.
*/
export interface IOptions extends Layout.IOptions {
/**
* The initial row count for the layout.
*
* The default is `1`.
*/
rowCount?: number;
/**
* The initial column count for the layout.
*
* The default is `1`.
*/
columnCount?: number;
/**
* The spacing between rows in the layout.
*
* The default is `4`.
*/
rowSpacing?: number;
/**
* The spacing between columns in the layout.
*
* The default is `4`.
*/
columnSpacing?: number;
}
/**
* An object which holds the cell configuration for a widget.
*/
export interface ICellConfig {
/**
* The row index for the widget.
*/
readonly row: number;
/**
* The column index for the widget.
*/
readonly column: number;
/**
* The row span for the widget.
*/
readonly rowSpan: number;
/**
* The column span for the widget.
*/
readonly columnSpan: number;
}
/**
* Get the cell config for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The cell config for the widget.
*/
export function getCellConfig(widget: Widget): ICellConfig {
return Private.cellConfigProperty.get(widget);
}
/**
* Set the cell config for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the cell config.
*/
export function setCellConfig(
widget: Widget,
value: Partial<ICellConfig>,
): void {
Private.cellConfigProperty.set(widget, Private.normalizeConfig(value));
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* The property descriptor for the widget cell config.
*/
export const cellConfigProperty = new AttachedProperty<
Widget,
GridLayout.ICellConfig
>({
name: 'cellConfig',
create: () => ({ row: 0, column: 0, rowSpan: 1, columnSpan: 1 }),
changed: onChildCellConfigChanged,
});
/**
* Normalize a partial cell config object.
*/
export function normalizeConfig(
config: Partial<GridLayout.ICellConfig>,
): GridLayout.ICellConfig {
const row = Math.max(0, Math.floor(config.row || 0));
const column = Math.max(0, Math.floor(config.column || 0));
const rowSpan = Math.max(1, Math.floor(config.rowSpan || 0));
const columnSpan = Math.max(1, Math.floor(config.columnSpan || 0));
return { row, column, rowSpan, columnSpan };
}
/**
* Clamp a value to an integer >= 0.
*/
export function clampValue(value: number): number {
return Math.max(0, Math.floor(value));
}
/**
* A sort comparison function for row spans.
*/
export function rowSpanCmp(a: LayoutItem, b: LayoutItem): number {
const c1 = cellConfigProperty.get(a.widget);
const c2 = cellConfigProperty.get(b.widget);
return c1.rowSpan - c2.rowSpan;
}
/**
* A sort comparison function for column spans.
*/
export function columnSpanCmp(a: LayoutItem, b: LayoutItem): number {
const c1 = cellConfigProperty.get(a.widget);
const c2 = cellConfigProperty.get(b.widget);
return c1.columnSpan - c2.columnSpan;
}
/**
* Reallocate the box sizers for the given grid dimensions.
*/
export function reallocSizers(sizers: BoxSizer[], count: number): void {
// Coerce the count to the valid range.
count = Math.max(1, Math.floor(count));
// Add the missing sizers.
while (sizers.length < count) {
sizers.push(new BoxSizer());
}
// Remove the extra sizers.
if (sizers.length > count) {
sizers.length = count;
}
}
/**
* Distribute a min size constraint across a range of sizers.
*/
export function distributeMin(
sizers: BoxSizer[],
i1: number,
i2: number,
minSize: number,
): void {
// Sanity check the indices.
if (i2 < i1) {
return;
}
// Handle the simple case of no cell span.
if (i1 === i2) {
const sizer = sizers[i1];
sizer.minSize = Math.max(sizer.minSize, minSize);
return;
}
// Compute the total current min size of the span.
let totalMin = 0;
for (let i = i1; i <= i2; ++i) {
totalMin += sizers[i].minSize;
}
// Do nothing if the total is greater than the required.
if (totalMin >= minSize) {
return;
}
// Compute the portion of the space to allocate to each sizer.
const portion = (minSize - totalMin) / (i2 - i1 + 1);
// Add the portion to each sizer.
for (let i = i1; i <= i2; ++i) {
sizers[i].minSize += portion;
}
}
/**
* The change handler for the child cell config property.
*/
function onChildCellConfigChanged(child: Widget): void {
if (child.parent && child.parent.layout instanceof GridLayout) {
child.parent.fit();
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module widgets
*/
export * from './boxengine';
export * from './boxlayout';
export * from './boxpanel';
export * from './docklayout';
export * from './dockpanel';
export * from './focustracker';
export * from './gridlayout';
export * from './layout';
export * from './panel';
export * from './panellayout';
export * from './scrollbar';
export * from './singletonlayout';
export * from './splitlayout';
export * from './splitpanel';
export * from './stackedlayout';
export * from './stackedpanel';
export * from './tabbar';
export * from './tabpanel';
export * from './title';
export * from './widget';

View File

@@ -0,0 +1,891 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { Signal } from '../signaling';
import { AttachedProperty } from '../properties';
import { type Message, MessageLoop } from '../messaging';
import { ElementExt } from '../domutils';
import { type IDisposable } from '../disposable';
import { Widget } from './widget';
/**
* An abstract base class for creating lumino layouts.
*
* #### Notes
* A layout is used to add widgets to a parent and to arrange those
* widgets within the parent's DOM node.
*
* This class implements the base functionality which is required of
* nearly all layouts. It must be subclassed in order to be useful.
*
* Notably, this class does not define a uniform interface for adding
* widgets to the layout. A subclass should define that API in a way
* which is meaningful for its intended use.
*/
export abstract class Layout implements Iterable<Widget>, IDisposable {
/**
* Construct a new layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options: Layout.IOptions = {}) {
this._fitPolicy = options.fitPolicy || 'set-min-size';
}
/**
* Dispose of the resources held by the layout.
*
* #### Notes
* This should be reimplemented to clear and dispose of the widgets.
*
* All reimplementations should call the superclass method.
*
* This method is called automatically when the parent is disposed.
*/
dispose(): void {
this._parent = null;
this._disposed = true;
Signal.clearData(this);
AttachedProperty.clearData(this);
}
/**
* Test whether the layout is disposed.
*/
get isDisposed(): boolean {
return this._disposed;
}
/**
* Get the parent widget of the layout.
*/
get parent(): Widget | null {
return this._parent;
}
/**
* Set the parent widget of the layout.
*
* #### Notes
* This is set automatically when installing the layout on the parent
* widget. The parent widget should not be set directly by user code.
*/
set parent(value: Widget | null) {
if (this._parent === value) {
return;
}
if (this._parent) {
throw new Error('Cannot change parent widget.');
}
if (value!.layout !== this) {
throw new Error('Invalid parent widget.');
}
this._parent = value;
this.init();
}
/**
* Get the fit policy for the layout.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*/
get fitPolicy(): Layout.FitPolicy {
return this._fitPolicy;
}
/**
* Set the fit policy for the layout.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*
* Changing the fit policy will clear the current size constraint
* for the parent widget and then re-fit the parent.
*/
set fitPolicy(value: Layout.FitPolicy) {
// Bail if the policy does not change
if (this._fitPolicy === value) {
return;
}
// Update the internal policy.
this._fitPolicy = value;
// Clear the size constraints and schedule a fit of the parent.
if (this._parent) {
const { style } = this._parent.node;
style.minWidth = '';
style.minHeight = '';
style.maxWidth = '';
style.maxHeight = '';
this._parent.fit();
}
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*
* #### Notes
* This abstract method must be implemented by a subclass.
*/
abstract [Symbol.iterator](): IterableIterator<Widget>;
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method should *not* modify the widget's `parent`.
*/
abstract removeWidget(widget: Widget): void;
/**
* Process a message sent to the parent widget.
*
* @param msg - The message sent to the parent widget.
*
* #### Notes
* This method is called by the parent widget to process a message.
*
* Subclasses may reimplement this method as needed.
*/
processParentMessage(msg: Message): void {
switch (msg.type) {
case 'resize':
this.onResize(msg as Widget.ResizeMessage);
break;
case 'update-request':
this.onUpdateRequest(msg);
break;
case 'fit-request':
this.onFitRequest(msg);
break;
case 'before-show':
this.onBeforeShow(msg);
break;
case 'after-show':
this.onAfterShow(msg);
break;
case 'before-hide':
this.onBeforeHide(msg);
break;
case 'after-hide':
this.onAfterHide(msg);
break;
case 'before-attach':
this.onBeforeAttach(msg);
break;
case 'after-attach':
this.onAfterAttach(msg);
break;
case 'before-detach':
this.onBeforeDetach(msg);
break;
case 'after-detach':
this.onAfterDetach(msg);
break;
case 'child-removed':
this.onChildRemoved(msg as Widget.ChildMessage);
break;
case 'child-shown':
this.onChildShown(msg as Widget.ChildMessage);
break;
case 'child-hidden':
this.onChildHidden(msg as Widget.ChildMessage);
break;
}
}
/**
* Perform layout initialization which requires the parent widget.
*
* #### Notes
* This method is invoked immediately after the layout is installed
* on the parent widget.
*
* The default implementation reparents all of the widgets to the
* layout parent widget.
*
* Subclasses should reimplement this method and attach the child
* widget nodes to the parent widget's node.
*/
protected init(): void {
for (const widget of this) {
widget.parent = this.parent;
}
}
/**
* A message handler invoked on a `'resize'` message.
*
* #### Notes
* The layout should ensure that its widgets are resized according
* to the specified layout space, and that they are sent a `'resize'`
* message if appropriate.
*
* The default implementation of this method sends an `UnknownSize`
* resize message to all widgets.
*
* This may be reimplemented by subclasses as needed.
*/
protected onResize(msg: Widget.ResizeMessage): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*
* #### Notes
* The layout should ensure that its widgets are resized according
* to the available layout space, and that they are sent a `'resize'`
* message if appropriate.
*
* The default implementation of this method sends an `UnknownSize`
* resize message to all widgets.
*
* This may be reimplemented by subclasses as needed.
*/
protected onUpdateRequest(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, Widget.ResizeMessage.UnknownSize);
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeAttach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on an `'after-attach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterAttach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on a `'before-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeDetach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on an `'after-detach'` message.
*
* #### Notes
* The default implementation of this method forwards the message
* to all widgets. It assumes all widget nodes are attached to the
* parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterDetach(msg: Message): void {
for (const widget of this) {
MessageLoop.sendMessage(widget, msg);
}
}
/**
* A message handler invoked on a `'before-show'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeShow(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on an `'after-show'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterShow(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on a `'before-hide'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onBeforeHide(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on an `'after-hide'` message.
*
* #### Notes
* The default implementation of this method forwards the message to
* all non-hidden widgets. It assumes all widget nodes are attached
* to the parent widget node.
*
* This may be reimplemented by subclasses as needed.
*/
protected onAfterHide(msg: Message): void {
for (const widget of this) {
if (!widget.isHidden) {
MessageLoop.sendMessage(widget, msg);
}
}
}
/**
* A message handler invoked on a `'child-removed'` message.
*
* #### Notes
* This will remove the child widget from the layout.
*
* Subclasses should **not** typically reimplement this method.
*/
protected onChildRemoved(msg: Widget.ChildMessage): void {
this.removeWidget(msg.child);
}
/**
* A message handler invoked on a `'fit-request'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
protected onFitRequest(msg: Message): void {}
/**
* A message handler invoked on a `'child-shown'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
protected onChildShown(msg: Widget.ChildMessage): void {}
/**
* A message handler invoked on a `'child-hidden'` message.
*
* #### Notes
* The default implementation of this handler is a no-op.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {}
private _disposed = false;
private _fitPolicy: Layout.FitPolicy;
private _parent: Widget | null = null;
}
/**
* The namespace for the `Layout` class statics.
*/
export namespace Layout {
/**
* A type alias for the layout fit policy.
*
* #### Notes
* The fit policy controls the computed size constraints which are
* applied to the parent widget by the layout.
*
* Some layout implementations may ignore the fit policy.
*/
export type FitPolicy =
| /**
* No size constraint will be applied to the parent widget.
*/
'set-no-constraint'
/**
* The computed min size will be applied to the parent widget.
*/
| 'set-min-size';
/**
* An options object for initializing a layout.
*/
export interface IOptions {
/**
* The fit policy for the layout.
*
* The default is `'set-min-size'`.
*/
fitPolicy?: FitPolicy;
}
/**
* A type alias for the horizontal alignment of a widget.
*/
export type HorizontalAlignment = 'left' | 'center' | 'right';
/**
* A type alias for the vertical alignment of a widget.
*/
export type VerticalAlignment = 'top' | 'center' | 'bottom';
/**
* Get the horizontal alignment for a widget.
*
* @param widget - The widget of interest.
*
* @returns The horizontal alignment for the widget.
*
* #### Notes
* If the layout width allocated to a widget is larger than its max
* width, the horizontal alignment controls how the widget is placed
* within the extra horizontal space.
*
* If the allocated width is less than the widget's max width, the
* horizontal alignment has no effect.
*
* Some layout implementations may ignore horizontal alignment.
*/
export function getHorizontalAlignment(widget: Widget): HorizontalAlignment {
return Private.horizontalAlignmentProperty.get(widget);
}
/**
* Set the horizontal alignment for a widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the horizontal alignment.
*
* #### Notes
* If the layout width allocated to a widget is larger than its max
* width, the horizontal alignment controls how the widget is placed
* within the extra horizontal space.
*
* If the allocated width is less than the widget's max width, the
* horizontal alignment has no effect.
*
* Some layout implementations may ignore horizontal alignment.
*
* Changing the horizontal alignment will post an `update-request`
* message to widget's parent, provided the parent has a layout
* installed.
*/
export function setHorizontalAlignment(
widget: Widget,
value: HorizontalAlignment,
): void {
Private.horizontalAlignmentProperty.set(widget, value);
}
/**
* Get the vertical alignment for a widget.
*
* @param widget - The widget of interest.
*
* @returns The vertical alignment for the widget.
*
* #### Notes
* If the layout height allocated to a widget is larger than its max
* height, the vertical alignment controls how the widget is placed
* within the extra vertical space.
*
* If the allocated height is less than the widget's max height, the
* vertical alignment has no effect.
*
* Some layout implementations may ignore vertical alignment.
*/
export function getVerticalAlignment(widget: Widget): VerticalAlignment {
return Private.verticalAlignmentProperty.get(widget);
}
/**
* Set the vertical alignment for a widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the vertical alignment.
*
* #### Notes
* If the layout height allocated to a widget is larger than its max
* height, the vertical alignment controls how the widget is placed
* within the extra vertical space.
*
* If the allocated height is less than the widget's max height, the
* vertical alignment has no effect.
*
* Some layout implementations may ignore vertical alignment.
*
* Changing the horizontal alignment will post an `update-request`
* message to widget's parent, provided the parent has a layout
* installed.
*/
export function setVerticalAlignment(
widget: Widget,
value: VerticalAlignment,
): void {
Private.verticalAlignmentProperty.set(widget, value);
}
}
/**
* An object which assists in the absolute layout of widgets.
*
* #### Notes
* This class is useful when implementing a layout which arranges its
* widgets using absolute positioning.
*
* This class is used by nearly all of the built-in lumino layouts.
*/
export class LayoutItem implements IDisposable {
/**
* Construct a new layout item.
*
* @param widget - The widget to be managed by the item.
*
* #### Notes
* The widget will be set to absolute positioning.
* The widget will use strict CSS containment.
*/
constructor(widget: Widget) {
this.widget = widget;
this.widget.node.style.position = 'absolute';
this.widget.node.style.contain = 'strict';
}
/**
* Dispose of the the layout item.
*
* #### Notes
* This will reset the positioning of the widget.
*/
dispose(): void {
// Do nothing if the item is already disposed.
if (this._disposed) {
return;
}
// Mark the item as disposed.
this._disposed = true;
// Reset the widget style.
const { style } = this.widget.node;
style.position = '';
style.top = '';
style.left = '';
style.width = '';
style.height = '';
style.contain = '';
}
/**
* The widget managed by the layout item.
*/
readonly widget: Widget;
/**
* The computed minimum width of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get minWidth(): number {
return this._minWidth;
}
/**
* The computed minimum height of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get minHeight(): number {
return this._minHeight;
}
/**
* The computed maximum width of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get maxWidth(): number {
return this._maxWidth;
}
/**
* The computed maximum height of the widget.
*
* #### Notes
* This value can be updated by calling the `fit` method.
*/
get maxHeight(): number {
return this._maxHeight;
}
/**
* Whether the layout item is disposed.
*/
get isDisposed(): boolean {
return this._disposed;
}
/**
* Whether the managed widget is hidden.
*/
get isHidden(): boolean {
return this.widget.isHidden;
}
/**
* Whether the managed widget is visible.
*/
get isVisible(): boolean {
return this.widget.isVisible;
}
/**
* Whether the managed widget is attached.
*/
get isAttached(): boolean {
return this.widget.isAttached;
}
/**
* Update the computed size limits of the managed widget.
*/
fit(): void {
const limits = ElementExt.sizeLimits(this.widget.node);
this._minWidth = limits.minWidth;
this._minHeight = limits.minHeight;
this._maxWidth = limits.maxWidth;
this._maxHeight = limits.maxHeight;
}
/**
* Update the position and size of the managed widget.
*
* @param left - The left edge position of the layout box.
*
* @param top - The top edge position of the layout box.
*
* @param width - The width of the layout box.
*
* @param height - The height of the layout box.
*/
update(left: number, top: number, width: number, height: number): void {
// Clamp the size to the computed size limits.
const clampW = Math.max(this._minWidth, Math.min(width, this._maxWidth));
const clampH = Math.max(this._minHeight, Math.min(height, this._maxHeight));
// Adjust the left edge for the horizontal alignment, if needed.
if (clampW < width) {
switch (Layout.getHorizontalAlignment(this.widget)) {
case 'left':
break;
case 'center':
left += (width - clampW) / 2;
break;
case 'right':
left += width - clampW;
break;
default:
throw 'unreachable';
}
}
// Adjust the top edge for the vertical alignment, if needed.
if (clampH < height) {
switch (Layout.getVerticalAlignment(this.widget)) {
case 'top':
break;
case 'center':
top += (height - clampH) / 2;
break;
case 'bottom':
top += height - clampH;
break;
default:
throw 'unreachable';
}
}
// Set up the resize variables.
let resized = false;
const { style } = this.widget.node;
// Update the top edge of the widget if needed.
if (this._top !== top) {
this._top = top;
style.top = `${top}px`;
}
// Update the left edge of the widget if needed.
if (this._left !== left) {
this._left = left;
style.left = `${left}px`;
}
// Update the width of the widget if needed.
if (this._width !== clampW) {
resized = true;
this._width = clampW;
style.width = `${clampW}px`;
}
// Update the height of the widget if needed.
if (this._height !== clampH) {
resized = true;
this._height = clampH;
style.height = `${clampH}px`;
}
// Send a resize message to the widget if needed.
if (resized) {
const msg = new Widget.ResizeMessage(clampW, clampH);
MessageLoop.sendMessage(this.widget, msg);
}
}
private _top = NaN;
private _left = NaN;
private _width = NaN;
private _height = NaN;
private _minWidth = 0;
private _minHeight = 0;
private _maxWidth = Infinity;
private _maxHeight = Infinity;
private _disposed = false;
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* The attached property for a widget horizontal alignment.
*/
export const horizontalAlignmentProperty = new AttachedProperty<
Widget,
Layout.HorizontalAlignment
>({
name: 'horizontalAlignment',
create: () => 'center',
changed: onAlignmentChanged,
});
/**
* The attached property for a widget vertical alignment.
*/
export const verticalAlignmentProperty = new AttachedProperty<
Widget,
Layout.VerticalAlignment
>({
name: 'verticalAlignment',
create: () => 'top',
changed: onAlignmentChanged,
});
/**
* The change handler for the attached alignment properties.
*/
function onAlignmentChanged(child: Widget): void {
if (child.parent && child.parent.layout) {
child.parent.update();
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { Widget } from './widget';
import { PanelLayout } from './panellayout';
/**
* A simple and convenient panel widget class.
*
* #### Notes
* This class is suitable as a base class for implementing a variety of
* convenience panel widgets, but can also be used directly with CSS to
* arrange a collection of widgets.
*
* This class provides a convenience wrapper around a {@link PanelLayout}.
*/
export class Panel extends Widget {
/**
* Construct a new panel.
*
* @param options - The options for initializing the panel.
*/
constructor(options: Panel.IOptions = {}) {
super();
this.addClass('lm-Panel');
this.layout = Private.createLayout(options);
}
/**
* A read-only array of the widgets in the panel.
*/
get widgets(): ReadonlyArray<Widget> {
return (this.layout as PanelLayout).widgets;
}
/**
* Add a widget to the end of the panel.
*
* @param widget - The widget to add to the panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*/
addWidget(widget: Widget): void {
(this.layout as PanelLayout).addWidget(widget);
}
/**
* Insert a widget at the specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into to the panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*/
insertWidget(index: number, widget: Widget): void {
(this.layout as PanelLayout).insertWidget(index, widget);
}
}
/**
* The namespace for the `Panel` class statics.
*/
export namespace Panel {
/**
* An options object for creating a panel.
*/
export interface IOptions {
/**
* The panel layout to use for the panel.
*
* The default is a new `PanelLayout`.
*/
layout?: PanelLayout;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* Create a panel layout for the given panel options.
*/
export function createLayout(options: Panel.IOptions): PanelLayout {
return options.layout || new PanelLayout();
}
}

View File

@@ -0,0 +1,325 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { MessageLoop } from '../messaging';
import { ArrayExt } from '../algorithm';
import { Widget } from './widget';
import { Layout } from './layout';
/**
* A concrete layout implementation suitable for many use cases.
*
* #### Notes
* This class is suitable as a base class for implementing a variety of
* layouts, but can also be used directly with standard CSS to layout a
* collection of widgets.
*/
export class PanelLayout extends Layout {
/**
* Dispose of the resources held by the layout.
*
* #### Notes
* This will clear and dispose all widgets in the layout.
*
* All reimplementations should call the superclass method.
*
* This method is called automatically when the parent is disposed.
*/
dispose(): void {
while (this._widgets.length > 0) {
this._widgets.pop()!.dispose();
}
super.dispose();
}
/**
* A read-only array of the widgets in the layout.
*/
get widgets(): ReadonlyArray<Widget> {
return this._widgets;
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
*[Symbol.iterator](): IterableIterator<Widget> {
yield* this._widgets;
}
/**
* Add a widget to the end of the layout.
*
* @param widget - The widget to add to the layout.
*
* #### Notes
* If the widget is already contained in the layout, it will be moved.
*/
addWidget(widget: Widget): void {
this.insertWidget(this._widgets.length, widget);
}
/**
* Insert a widget into the layout at the specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into the layout.
*
* #### Notes
* The index will be clamped to the bounds of the widgets.
*
* If the widget is already added to the layout, it will be moved.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*/
insertWidget(index: number, widget: Widget): void {
// Remove the widget from its current parent. This is a no-op
// if the widget's parent is already the layout parent widget.
widget.parent = this.parent;
// Look up the current index of the widget.
const i = this._widgets.indexOf(widget);
// Clamp the insert index to the array bounds.
let j = Math.max(0, Math.min(index, this._widgets.length));
// If the widget is not in the array, insert it.
if (i === -1) {
// Insert the widget into the array.
ArrayExt.insert(this._widgets, j, widget);
// If the layout is parented, attach the widget to the DOM.
if (this.parent) {
this.attachWidget(j, widget);
}
// There is nothing more to do.
return;
}
// Otherwise, the widget exists in the array and should be moved.
// Adjust the index if the location is at the end of the array.
if (j === this._widgets.length) {
j--;
}
// Bail if there is no effective move.
if (i === j) {
return;
}
// Move the widget to the new location.
ArrayExt.move(this._widgets, i, j);
// If the layout is parented, move the widget in the DOM.
if (this.parent) {
this.moveWidget(i, j, widget);
}
}
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget: Widget): void {
this.removeWidgetAt(this._widgets.indexOf(widget));
}
/**
* Remove the widget at a given index from the layout.
*
* @param index - The index of the widget to remove.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*
* #### Undefined Behavior
* An `index` which is non-integral.
*/
removeWidgetAt(index: number): void {
// Remove the widget from the array.
const widget = ArrayExt.removeAt(this._widgets, index);
// If the layout is parented, detach the widget from the DOM.
if (widget && this.parent) {
this.detachWidget(index, widget);
}
}
/**
* Perform layout initialization which requires the parent widget.
*/
protected init(): void {
super.init();
let index = 0;
for (const widget of this) {
this.attachWidget(index++, widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation adds the widgets's node to the parent's
* node at the proper location, and sends the appropriate attach
* messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is added to the parent's node.
*/
protected attachWidget(index: number, widget: Widget): void {
// Look up the next sibling reference node.
const ref = this.parent!.node.children[index];
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Insert the widget's node before the sibling.
this.parent!.node.insertBefore(widget.node, ref);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation moves the widget's node to the proper
* location in the parent's node and sends the appropriate attach and
* detach messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is moved in the parent's node.
*/
protected moveWidget(
fromIndex: number,
toIndex: number,
widget: Widget,
): void {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` and message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Look up the next sibling reference node.
const ref = this.parent!.node.children[toIndex];
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Insert the widget's node before the sibling.
this.parent!.node.insertBefore(widget.node, ref);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This method is called automatically by the panel layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation removes the widget's node from the
* parent's node, and sends the appropriate detach messages to the
* widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is removed from the parent's node.
*/
protected detachWidget(index: number, widget: Widget): void {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
}
private _widgets: Widget[] = [];
}

View File

@@ -0,0 +1,854 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type ISignal, Signal } from '../signaling';
import { type Message } from '../messaging';
import { Drag } from '../dragdrop';
import { ElementExt } from '../domutils';
import { type IDisposable } from '../disposable';
import { Widget } from './widget';
/**
* A widget which implements a canonical scroll bar.
*/
export class ScrollBar extends Widget {
/**
* Construct a new scroll bar.
*
* @param options - The options for initializing the scroll bar.
*/
constructor(options: ScrollBar.IOptions = {}) {
super({ node: Private.createNode() });
this.addClass('lm-ScrollBar');
this.setFlag(Widget.Flag.DisallowLayout);
// Set the orientation.
this._orientation = options.orientation || 'vertical';
this.dataset.orientation = this._orientation;
// Parse the rest of the options.
if (options.maximum !== undefined) {
this._maximum = Math.max(0, options.maximum);
}
if (options.page !== undefined) {
this._page = Math.max(0, options.page);
}
if (options.value !== undefined) {
this._value = Math.max(0, Math.min(options.value, this._maximum));
}
}
/**
* A signal emitted when the user moves the scroll thumb.
*
* #### Notes
* The payload is the current value of the scroll bar.
*/
get thumbMoved(): ISignal<this, number> {
return this._thumbMoved;
}
/**
* A signal emitted when the user clicks a step button.
*
* #### Notes
* The payload is whether a decrease or increase is requested.
*/
get stepRequested(): ISignal<this, 'decrement' | 'increment'> {
return this._stepRequested;
}
/**
* A signal emitted when the user clicks the scroll track.
*
* #### Notes
* The payload is whether a decrease or increase is requested.
*/
get pageRequested(): ISignal<this, 'decrement' | 'increment'> {
return this._pageRequested;
}
/**
* Get the orientation of the scroll bar.
*/
get orientation(): ScrollBar.Orientation {
return this._orientation;
}
/**
* Set the orientation of the scroll bar.
*/
set orientation(value: ScrollBar.Orientation) {
// Do nothing if the orientation does not change.
if (this._orientation === value) {
return;
}
// Release the mouse before making changes.
this._releaseMouse();
// Update the internal orientation.
this._orientation = value;
this.dataset.orientation = value;
// Schedule an update the scroll bar.
this.update();
}
/**
* Get the current value of the scroll bar.
*/
get value(): number {
return this._value;
}
/**
* Set the current value of the scroll bar.
*
* #### Notes
* The value will be clamped to the range `[0, maximum]`.
*/
set value(value: number) {
// Clamp the value to the allowable range.
value = Math.max(0, Math.min(value, this._maximum));
// Do nothing if the value does not change.
if (this._value === value) {
return;
}
// Update the internal value.
this._value = value;
// Schedule an update the scroll bar.
this.update();
}
/**
* Get the page size of the scroll bar.
*
* #### Notes
* The page size is the amount of visible content in the scrolled
* region, expressed in data units. It determines the size of the
* scroll bar thumb.
*/
get page(): number {
return this._page;
}
/**
* Set the page size of the scroll bar.
*
* #### Notes
* The page size will be clamped to the range `[0, Infinity]`.
*/
set page(value: number) {
// Clamp the page size to the allowable range.
value = Math.max(0, value);
// Do nothing if the value does not change.
if (this._page === value) {
return;
}
// Update the internal page size.
this._page = value;
// Schedule an update the scroll bar.
this.update();
}
/**
* Get the maximum value of the scroll bar.
*/
get maximum(): number {
return this._maximum;
}
/**
* Set the maximum value of the scroll bar.
*
* #### Notes
* The max size will be clamped to the range `[0, Infinity]`.
*/
set maximum(value: number) {
// Clamp the value to the allowable range.
value = Math.max(0, value);
// Do nothing if the value does not change.
if (this._maximum === value) {
return;
}
// Update the internal values.
this._maximum = value;
// Clamp the current value to the new range.
this._value = Math.min(this._value, value);
// Schedule an update the scroll bar.
this.update();
}
/**
* The scroll bar decrement button node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get decrementNode(): HTMLDivElement {
return this.node.getElementsByClassName(
'lm-ScrollBar-button',
)[0] as HTMLDivElement;
}
/**
* The scroll bar increment button node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get incrementNode(): HTMLDivElement {
return this.node.getElementsByClassName(
'lm-ScrollBar-button',
)[1] as HTMLDivElement;
}
/**
* The scroll bar track node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get trackNode(): HTMLDivElement {
return this.node.getElementsByClassName(
'lm-ScrollBar-track',
)[0] as HTMLDivElement;
}
/**
* The scroll bar thumb node.
*
* #### Notes
* Modifying this node directly can lead to undefined behavior.
*/
get thumbNode(): HTMLDivElement {
return this.node.getElementsByClassName(
'lm-ScrollBar-thumb',
)[0] as HTMLDivElement;
}
/**
* Handle the DOM events for the scroll bar.
*
* @param event - The DOM event sent to the scroll bar.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the scroll bar's DOM node.
*
* This should not be called directly by user code.
*/
handleEvent(event: Event): void {
switch (event.type) {
case 'mousedown':
this._evtMouseDown(event as MouseEvent);
break;
case 'mousemove':
this._evtMouseMove(event as MouseEvent);
break;
case 'mouseup':
this._evtMouseUp(event as MouseEvent);
break;
case 'keydown':
this._evtKeyDown(event as KeyboardEvent);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A method invoked on a 'before-attach' message.
*/
protected onBeforeAttach(msg: Message): void {
this.node.addEventListener('mousedown', this);
this.update();
}
/**
* A method invoked on an 'after-detach' message.
*/
protected onAfterDetach(msg: Message): void {
this.node.removeEventListener('mousedown', this);
this._releaseMouse();
}
/**
* A method invoked on an 'update-request' message.
*/
protected onUpdateRequest(msg: Message): void {
// Convert the value and page into percentages.
let value = (this._value * 100) / this._maximum;
let page = (this._page * 100) / (this._page + this._maximum);
// Clamp the value and page to the relevant range.
value = Math.max(0, Math.min(value, 100));
page = Math.max(0, Math.min(page, 100));
// Fetch the thumb style.
const thumbStyle = this.thumbNode.style;
// Update the thumb style for the current orientation.
if (this._orientation === 'horizontal') {
thumbStyle.top = '';
thumbStyle.height = '';
thumbStyle.left = `${value}%`;
thumbStyle.width = `${page}%`;
thumbStyle.transform = `translate(${-value}%, 0%)`;
} else {
thumbStyle.left = '';
thumbStyle.width = '';
thumbStyle.top = `${value}%`;
thumbStyle.height = `${page}%`;
thumbStyle.transform = `translate(0%, ${-value}%)`;
}
}
/**
* Handle the `'keydown'` event for the scroll bar.
*/
private _evtKeyDown(event: KeyboardEvent): void {
// Stop all input events during drag.
event.preventDefault();
event.stopPropagation();
// Ignore anything except the `Escape` key.
if (event.keyCode !== 27) {
return;
}
// Fetch the previous scroll value.
const value = this._pressData ? this._pressData.value : -1;
// Release the mouse.
this._releaseMouse();
// Restore the old scroll value if possible.
if (value !== -1) {
this._moveThumb(value);
}
}
/**
* Handle the `'mousedown'` event for the scroll bar.
*/
private _evtMouseDown(event: MouseEvent): void {
// Do nothing if it's not a left mouse press.
if (event.button !== 0) {
return;
}
// Send an activate request to the scroll bar. This can be
// used by message hooks to activate something relevant.
this.activate();
// Do nothing if the mouse is already captured.
if (this._pressData) {
return;
}
// Find the pressed scroll bar part.
const part = Private.findPart(this, event.target as HTMLElement);
// Do nothing if the part is not of interest.
if (!part) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Override the mouse cursor.
const override = Drag.overrideCursor('default');
// Set up the press data.
this._pressData = {
part,
override,
delta: -1,
value: -1,
mouseX: event.clientX,
mouseY: event.clientY,
};
// Add the extra event listeners.
document.addEventListener('mousemove', this, true);
document.addEventListener('mouseup', this, true);
document.addEventListener('keydown', this, true);
document.addEventListener('contextmenu', this, true);
// Handle a thumb press.
if (part === 'thumb') {
// Fetch the thumb node.
const { thumbNode } = this;
// Fetch the client rect for the thumb.
const thumbRect = thumbNode.getBoundingClientRect();
// Update the press data delta for the current orientation.
if (this._orientation === 'horizontal') {
this._pressData.delta = event.clientX - thumbRect.left;
} else {
this._pressData.delta = event.clientY - thumbRect.top;
}
// Add the active class to the thumb node.
thumbNode.classList.add('lm-mod-active');
// Store the current value in the press data.
this._pressData.value = this._value;
// Finished.
return;
}
// Handle a track press.
if (part === 'track') {
// Fetch the client rect for the thumb.
const thumbRect = this.thumbNode.getBoundingClientRect();
// Determine the direction for the page request.
let dir: 'decrement' | 'increment';
if (this._orientation === 'horizontal') {
dir = event.clientX < thumbRect.left ? 'decrement' : 'increment';
} else {
dir = event.clientY < thumbRect.top ? 'decrement' : 'increment';
}
// Start the repeat timer.
this._repeatTimer = window.setTimeout(this._onRepeat, 350);
// Emit the page requested signal.
this._pageRequested.emit(dir);
// Finished.
return;
}
// Handle a decrement button press.
if (part === 'decrement') {
// Add the active class to the decrement node.
this.decrementNode.classList.add('lm-mod-active');
// Start the repeat timer.
this._repeatTimer = window.setTimeout(this._onRepeat, 350);
// Emit the step requested signal.
this._stepRequested.emit('decrement');
// Finished.
return;
}
// Handle an increment button press.
if (part === 'increment') {
// Add the active class to the increment node.
this.incrementNode.classList.add('lm-mod-active');
// Start the repeat timer.
this._repeatTimer = window.setTimeout(this._onRepeat, 350);
// Emit the step requested signal.
this._stepRequested.emit('increment');
// Finished.
return;
}
}
/**
* Handle the `'mousemove'` event for the scroll bar.
*/
private _evtMouseMove(event: MouseEvent): void {
// Do nothing if no drag is in progress.
if (!this._pressData) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Update the mouse position.
this._pressData.mouseX = event.clientX;
this._pressData.mouseY = event.clientY;
// Bail if the thumb is not being dragged.
if (this._pressData.part !== 'thumb') {
return;
}
// Get the client rect for the thumb and track.
const thumbRect = this.thumbNode.getBoundingClientRect();
const trackRect = this.trackNode.getBoundingClientRect();
// Fetch the scroll geometry based on the orientation.
let trackPos: number;
let trackSpan: number;
if (this._orientation === 'horizontal') {
trackPos = event.clientX - trackRect.left - this._pressData.delta;
trackSpan = trackRect.width - thumbRect.width;
} else {
trackPos = event.clientY - trackRect.top - this._pressData.delta;
trackSpan = trackRect.height - thumbRect.height;
}
// Compute the desired value from the scroll geometry.
const value = trackSpan === 0 ? 0 : (trackPos * this._maximum) / trackSpan;
// Move the thumb to the computed value.
this._moveThumb(value);
}
/**
* Handle the `'mouseup'` event for the scroll bar.
*/
private _evtMouseUp(event: MouseEvent): void {
// Do nothing if it's not a left mouse release.
if (event.button !== 0) {
return;
}
// Stop the event propagation.
event.preventDefault();
event.stopPropagation();
// Release the mouse.
this._releaseMouse();
}
/**
* Release the mouse and restore the node states.
*/
private _releaseMouse(): void {
// Bail if there is no press data.
if (!this._pressData) {
return;
}
// Clear the repeat timer.
clearTimeout(this._repeatTimer);
this._repeatTimer = -1;
// Clear the press data.
this._pressData.override.dispose();
this._pressData = null;
// Remove the extra event listeners.
document.removeEventListener('mousemove', this, true);
document.removeEventListener('mouseup', this, true);
document.removeEventListener('keydown', this, true);
document.removeEventListener('contextmenu', this, true);
// Remove the active classes from the nodes.
this.thumbNode.classList.remove('lm-mod-active');
this.decrementNode.classList.remove('lm-mod-active');
this.incrementNode.classList.remove('lm-mod-active');
}
/**
* Move the thumb to the specified position.
*/
private _moveThumb(value: number): void {
// Clamp the value to the allowed range.
value = Math.max(0, Math.min(value, this._maximum));
// Bail if the value does not change.
if (this._value === value) {
return;
}
// Update the internal value.
this._value = value;
// Schedule an update of the scroll bar.
this.update();
// Emit the thumb moved signal.
this._thumbMoved.emit(value);
}
/**
* A timeout callback for repeating the mouse press.
*/
private _onRepeat = () => {
// Clear the repeat timer id.
this._repeatTimer = -1;
// Bail if the mouse has been released.
if (!this._pressData) {
return;
}
// Look up the part that was pressed.
const { part } = this._pressData;
// Bail if the thumb was pressed.
if (part === 'thumb') {
return;
}
// Schedule the timer for another repeat.
this._repeatTimer = window.setTimeout(this._onRepeat, 20);
// Get the current mouse position.
const { mouseX } = this._pressData;
const { mouseY } = this._pressData;
// Handle a decrement button repeat.
if (part === 'decrement') {
// Bail if the mouse is not over the button.
if (!ElementExt.hitTest(this.decrementNode, mouseX, mouseY)) {
return;
}
// Emit the step requested signal.
this._stepRequested.emit('decrement');
// Finished.
return;
}
// Handle an increment button repeat.
if (part === 'increment') {
// Bail if the mouse is not over the button.
if (!ElementExt.hitTest(this.incrementNode, mouseX, mouseY)) {
return;
}
// Emit the step requested signal.
this._stepRequested.emit('increment');
// Finished.
return;
}
// Handle a track repeat.
if (part === 'track') {
// Bail if the mouse is not over the track.
if (!ElementExt.hitTest(this.trackNode, mouseX, mouseY)) {
return;
}
// Fetch the thumb node.
const { thumbNode } = this;
// Bail if the mouse is over the thumb.
if (ElementExt.hitTest(thumbNode, mouseX, mouseY)) {
return;
}
// Fetch the client rect for the thumb.
const thumbRect = thumbNode.getBoundingClientRect();
// Determine the direction for the page request.
let dir: 'decrement' | 'increment';
if (this._orientation === 'horizontal') {
dir = mouseX < thumbRect.left ? 'decrement' : 'increment';
} else {
dir = mouseY < thumbRect.top ? 'decrement' : 'increment';
}
// Emit the page requested signal.
this._pageRequested.emit(dir);
// Finished.
return;
}
};
private _value = 0;
private _page = 10;
private _maximum = 100;
private _repeatTimer = -1;
private _orientation: ScrollBar.Orientation;
private _pressData: Private.IPressData | null = null;
private _thumbMoved = new Signal<this, number>(this);
private _stepRequested = new Signal<this, 'decrement' | 'increment'>(this);
private _pageRequested = new Signal<this, 'decrement' | 'increment'>(this);
}
/**
* The namespace for the `ScrollBar` class statics.
*/
export namespace ScrollBar {
/**
* A type alias for a scroll bar orientation.
*/
export type Orientation = 'horizontal' | 'vertical';
/**
* An options object for creating a scroll bar.
*/
export interface IOptions {
/**
* The orientation of the scroll bar.
*
* The default is `'vertical'`.
*/
orientation?: Orientation;
/**
* The value for the scroll bar.
*
* The default is `0`.
*/
value?: number;
/**
* The page size for the scroll bar.
*
* The default is `10`.
*/
page?: number;
/**
* The maximum value for the scroll bar.
*
* The default is `100`.
*/
maximum?: number;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* A type alias for the parts of a scroll bar.
*/
export type ScrollBarPart = 'thumb' | 'track' | 'decrement' | 'increment';
/**
* An object which holds mouse press data.
*/
export interface IPressData {
/**
* The scroll bar part which was pressed.
*/
part: ScrollBarPart;
/**
* The offset of the press in thumb coordinates, or -1.
*/
delta: number;
/**
* The scroll value at the time the thumb was pressed, or -1.
*/
value: number;
/**
* The disposable which will clear the override cursor.
*/
override: IDisposable;
/**
* The current X position of the mouse.
*/
mouseX: number;
/**
* The current Y position of the mouse.
*/
mouseY: number;
}
/**
* Create the DOM node for a scroll bar.
*/
export function createNode(): HTMLElement {
const node = document.createElement('div');
const decrement = document.createElement('div');
const increment = document.createElement('div');
const track = document.createElement('div');
const thumb = document.createElement('div');
decrement.className = 'lm-ScrollBar-button';
increment.className = 'lm-ScrollBar-button';
decrement.dataset.action = 'decrement';
increment.dataset.action = 'increment';
track.className = 'lm-ScrollBar-track';
thumb.className = 'lm-ScrollBar-thumb';
track.appendChild(thumb);
node.appendChild(decrement);
node.appendChild(track);
node.appendChild(increment);
return node;
}
/**
* Find the scroll bar part which contains the given target.
*/
export function findPart(
scrollBar: ScrollBar,
target: HTMLElement,
): ScrollBarPart | null {
// Test the thumb.
if (scrollBar.thumbNode.contains(target)) {
return 'thumb';
}
// Test the track.
if (scrollBar.trackNode.contains(target)) {
return 'track';
}
// Test the decrement button.
if (scrollBar.decrementNode.contains(target)) {
return 'decrement';
}
// Test the increment button.
if (scrollBar.incrementNode.contains(target)) {
return 'increment';
}
// Indicate no match.
return null;
}
}

View File

@@ -0,0 +1,205 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { MessageLoop } from '../messaging';
import { Widget } from './widget';
import { Layout } from './layout';
/**
* A concrete layout implementation which holds a single widget.
*
* #### Notes
* This class is useful for creating simple container widgets which
* hold a single child. The child should be positioned with CSS.
*/
export class SingletonLayout extends Layout {
/**
* Dispose of the resources held by the layout.
*/
dispose(): void {
if (this._widget) {
const widget = this._widget;
this._widget = null;
widget.dispose();
}
super.dispose();
}
/**
* Get the child widget for the layout.
*/
get widget(): Widget | null {
return this._widget;
}
/**
* Set the child widget for the layout.
*
* #### Notes
* Setting the child widget will cause the old child widget to be
* automatically disposed. If that is not desired, set the parent
* of the old child to `null` before assigning a new child.
*/
set widget(widget: Widget | null) {
// Remove the widget from its current parent. This is a no-op
// if the widget's parent is already the layout parent widget.
if (widget) {
widget.parent = this.parent;
}
// Bail early if the widget does not change.
if (this._widget === widget) {
return;
}
// Dispose of the old child widget.
if (this._widget) {
this._widget.dispose();
}
// Update the internal widget.
this._widget = widget;
// Attach the new child widget if needed.
if (this.parent && widget) {
this.attachWidget(widget);
}
}
/**
* Create an iterator over the widgets in the layout.
*
* @returns A new iterator over the widgets in the layout.
*/
*[Symbol.iterator](): IterableIterator<Widget> {
if (this._widget) {
yield this._widget;
}
}
/**
* Remove a widget from the layout.
*
* @param widget - The widget to remove from the layout.
*
* #### Notes
* A widget is automatically removed from the layout when its `parent`
* is set to `null`. This method should only be invoked directly when
* removing a widget from a layout which has yet to be installed on a
* parent widget.
*
* This method does *not* modify the widget's `parent`.
*/
removeWidget(widget: Widget): void {
// Bail early if the widget does not exist in the layout.
if (this._widget !== widget) {
return;
}
// Clear the internal widget.
this._widget = null;
// If the layout is parented, detach the widget from the DOM.
if (this.parent) {
this.detachWidget(widget);
}
}
/**
* Perform layout initialization which requires the parent widget.
*/
protected init(): void {
super.init();
for (const widget of this) {
this.attachWidget(widget);
}
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This method is called automatically by the single layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation adds the widgets's node to the parent's
* node at the proper location, and sends the appropriate attach
* messages to the widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is added to the parent's node.
*/
protected attachWidget(widget: Widget): void {
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent!.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
}
/**
* Detach a widget from the parent's DOM node.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This method is called automatically by the single layout at the
* appropriate time. It should not be called directly by user code.
*
* The default implementation removes the widget's node from the
* parent's node, and sends the appropriate detach messages to the
* widget if the parent is attached to the DOM.
*
* Subclasses may reimplement this method to control how the widget's
* node is removed from the parent's node.
*/
protected detachWidget(widget: Widget): void {
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
}
private _widget: Widget | null = null;
}

View File

@@ -0,0 +1,882 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { AttachedProperty } from '../properties';
import { type Message, MessageLoop } from '../messaging';
import { ElementExt } from '../domutils';
import { ArrayExt } from '../algorithm';
import { Widget } from './widget';
import { Utils } from './utils';
import { PanelLayout } from './panellayout';
import { LayoutItem } from './layout';
import { BoxEngine, BoxSizer } from './boxengine';
/**
* A layout which arranges its widgets into resizable sections.
*/
export class SplitLayout extends PanelLayout {
/**
* Construct a new split layout.
*
* @param options - The options for initializing the layout.
*/
constructor(options: SplitLayout.IOptions) {
super();
this.renderer = options.renderer;
if (options.orientation !== undefined) {
this._orientation = options.orientation;
}
if (options.alignment !== undefined) {
this._alignment = options.alignment;
}
if (options.spacing !== undefined) {
this._spacing = Utils.clampDimension(options.spacing);
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose(): void {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
this._sizers.length = 0;
this._handles.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* The renderer used by the split layout.
*/
readonly renderer: SplitLayout.IRenderer;
/**
* Get the layout orientation for the split layout.
*/
get orientation(): SplitLayout.Orientation {
return this._orientation;
}
/**
* Set the layout orientation for the split layout.
*/
set orientation(value: SplitLayout.Orientation) {
if (this._orientation === value) {
return;
}
this._orientation = value;
if (!this.parent) {
return;
}
this.parent.dataset.orientation = value;
this.parent.fit();
}
/**
* Get the content alignment for the split layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split layout.
*/
get alignment(): SplitLayout.Alignment {
return this._alignment;
}
/**
* Set the content alignment for the split layout.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split layout.
*/
set alignment(value: SplitLayout.Alignment) {
if (this._alignment === value) {
return;
}
this._alignment = value;
if (!this.parent) {
return;
}
this.parent.dataset.alignment = value;
this.parent.update();
}
/**
* Get the inter-element spacing for the split layout.
*/
get spacing(): number {
return this._spacing;
}
/**
* Set the inter-element spacing for the split layout.
*/
set spacing(value: number) {
value = Utils.clampDimension(value);
if (this._spacing === value) {
return;
}
this._spacing = value;
if (!this.parent) {
return;
}
this.parent.fit();
}
/**
* A read-only array of the split handles in the layout.
*/
get handles(): ReadonlyArray<HTMLDivElement> {
return this._handles;
}
/**
* Get the absolute sizes of the widgets in the layout.
*
* @returns A new array of the absolute sizes of the widgets.
*
* This method **does not** measure the DOM nodes.
*/
absoluteSizes(): number[] {
return this._sizers.map(sizer => sizer.size);
}
/**
* Get the relative sizes of the widgets in the layout.
*
* @returns A new array of the relative sizes of the widgets.
*
* #### Notes
* The returned sizes reflect the sizes of the widgets normalized
* relative to their siblings.
*
* This method **does not** measure the DOM nodes.
*/
relativeSizes(): number[] {
return Private.normalize(this._sizers.map(sizer => sizer.size));
}
/**
* Set the relative sizes for the widgets in the layout.
*
* @param sizes - The relative sizes for the widgets in the panel.
* @param update - Update the layout after setting relative sizes.
* Default is True.
*
* #### Notes
* Extra values are ignored, too few will yield an undefined layout.
*
* The actual geometry of the DOM nodes is updated asynchronously.
*/
setRelativeSizes(sizes: number[], update = true): void {
// Copy the sizes and pad with zeros as needed.
const n = this._sizers.length;
const temp = sizes.slice(0, n);
while (temp.length < n) {
temp.push(0);
}
// Normalize the padded sizes.
const normed = Private.normalize(temp);
// Apply the normalized sizes to the sizers.
for (let i = 0; i < n; ++i) {
const sizer = this._sizers[i];
sizer.sizeHint = normed[i];
sizer.size = normed[i];
}
// Set the flag indicating the sizes are normalized.
this._hasNormedSizes = true;
// Trigger an update of the parent widget.
if (update && this.parent) {
this.parent.update();
}
}
/**
* Move the offset position of a split handle.
*
* @param index - The index of the handle of the interest.
*
* @param position - The desired offset position of the handle.
*
* #### Notes
* The position is relative to the offset parent.
*
* This will move the handle as close as possible to the desired
* position. The sibling widgets will be adjusted as necessary.
*/
moveHandle(index: number, position: number): void {
// Bail if the index is invalid or the handle is hidden.
const handle = this._handles[index];
if (!handle || handle.classList.contains('lm-mod-hidden')) {
return;
}
// Compute the desired delta movement for the handle.
let delta: number;
if (this._orientation === 'horizontal') {
delta = position - handle.offsetLeft;
} else {
delta = position - handle.offsetTop;
}
// Bail if there is no handle movement.
if (delta === 0) {
return;
}
// Prevent widget resizing unless needed.
for (const sizer of this._sizers) {
if (sizer.size > 0) {
sizer.sizeHint = sizer.size;
}
}
// Adjust the sizers to reflect the handle movement.
BoxEngine.adjust(this._sizers, index, delta);
// Update the layout of the widgets.
if (this.parent) {
this.parent.update();
}
}
/**
* Perform layout initialization which requires the parent widget.
*/
protected init(): void {
this.parent!.dataset.orientation = this.orientation;
this.parent!.dataset.alignment = this.alignment;
super.init();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected attachWidget(index: number, widget: Widget): void {
// Create the item, handle, and sizer for the new widget.
const item = new LayoutItem(widget);
const handle = Private.createHandle(this.renderer);
const average = Private.averageSize(this._sizers);
const sizer = Private.createSizer(average);
// Insert the item, handle, and sizer into the internal arrays.
ArrayExt.insert(this._items, index, item);
ArrayExt.insert(this._sizers, index, sizer);
ArrayExt.insert(this._handles, index, handle);
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget and handle nodes to the parent.
this.parent!.node.appendChild(widget.node);
this.parent!.node.appendChild(handle);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected moveWidget(
fromIndex: number,
toIndex: number,
widget: Widget,
): void {
// Move the item, sizer, and handle for the widget.
ArrayExt.move(this._items, fromIndex, toIndex);
ArrayExt.move(this._sizers, fromIndex, toIndex);
ArrayExt.move(this._handles, fromIndex, toIndex);
// Post a fit request to the parent to show/hide last handle.
this.parent!.fit();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected detachWidget(index: number, widget: Widget): void {
// Remove the item, handle, and sizer for the widget.
const item = ArrayExt.removeAt(this._items, index);
const handle = ArrayExt.removeAt(this._handles, index);
ArrayExt.removeAt(this._sizers, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget and handle nodes from the parent.
this.parent!.node.removeChild(widget.node);
this.parent!.node.removeChild(handle!);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Dispose of the layout item.
item!.dispose();
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
protected onBeforeShow(msg: Message): void {
super.onBeforeShow(msg);
this.parent!.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
protected onBeforeAttach(msg: Message): void {
super.onBeforeAttach(msg);
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
protected onChildShown(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
protected onResize(msg: Widget.ResizeMessage): void {
if (this.parent!.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
protected onUpdateRequest(msg: Message): void {
if (this.parent!.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
protected onFitRequest(msg: Message): void {
if (this.parent!.isAttached) {
this._fit();
}
}
/**
* Update the item position.
*
* @param i Item index
* @param isHorizontal Whether the layout is horizontal or not
* @param left Left position in pixels
* @param top Top position in pixels
* @param height Item height
* @param width Item width
* @param size Item size
*/
protected updateItemPosition(
i: number,
isHorizontal: boolean,
left: number,
top: number,
height: number,
width: number,
size: number,
): void {
const item = this._items[i];
if (item.isHidden) {
return;
}
// Fetch the style for the handle.
const handleStyle = this._handles[i].style;
// Update the widget and handle, and advance the relevant edge.
if (isHorizontal) {
left += this.widgetOffset;
item.update(left, top, size, height);
left += size;
handleStyle.top = `${top}px`;
handleStyle.left = `${left}px`;
handleStyle.width = `${this._spacing}px`;
handleStyle.height = `${height}px`;
} else {
top += this.widgetOffset;
item.update(left, top, width, size);
top += size;
handleStyle.top = `${top}px`;
handleStyle.left = `${left}px`;
handleStyle.width = `${width}px`;
handleStyle.height = `${this._spacing}px`;
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
private _fit(): void {
// Update the handles and track the visible widget count.
let nVisible = 0;
let lastHandleIndex = -1;
for (let i = 0, n = this._items.length; i < n; ++i) {
if (this._items[i].isHidden) {
this._handles[i].classList.add('lm-mod-hidden');
} else {
this._handles[i].classList.remove('lm-mod-hidden');
lastHandleIndex = i;
nVisible++;
}
}
// Hide the handle for the last visible widget.
if (lastHandleIndex !== -1) {
this._handles[lastHandleIndex].classList.add('lm-mod-hidden');
}
// Update the fixed space for the visible items.
this._fixed =
this._spacing * Math.max(0, nVisible - 1) +
this.widgetOffset * this._items.length;
// Setup the computed minimum size.
const horz = this._orientation === 'horizontal';
let minW = horz ? this._fixed : 0;
let minH = horz ? 0 : this._fixed;
// Update the sizers and computed size limits.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item and corresponding box sizer.
const item = this._items[i];
const sizer = this._sizers[i];
// Prevent resizing unless necessary.
if (sizer.size > 0) {
sizer.sizeHint = sizer.size;
}
// If the item is hidden, it should consume zero size.
if (item.isHidden) {
sizer.minSize = 0;
sizer.maxSize = 0;
continue;
}
// Update the size limits for the item.
item.fit();
// Update the stretch factor.
sizer.stretch = SplitLayout.getStretch(item.widget);
// Update the sizer limits and computed min size.
if (horz) {
sizer.minSize = item.minWidth;
sizer.maxSize = item.maxWidth;
minW += item.minWidth;
minH = Math.max(minH, item.minHeight);
} else {
sizer.minSize = item.minHeight;
sizer.maxSize = item.maxHeight;
minH += item.minHeight;
minW = Math.max(minW, item.minWidth);
}
}
// Update the box sizing and add it to the computed min size.
const box = (this._box = ElementExt.boxSizing(this.parent!.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
const { style } = this.parent!.node;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent!.parent) {
MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
private _update(offsetWidth: number, offsetHeight: number): void {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0 && this.widgetOffset === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent!.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent!.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = ElementExt.boxSizing(this.parent!.node);
}
// Compute the actual layout bounds adjusted for border and padding.
let top = this._box.paddingTop;
let left = this._box.paddingLeft;
const width = offsetWidth - this._box.horizontalSum;
const height = offsetHeight - this._box.verticalSum;
// Set up the variables for justification and alignment offset.
let extra = 0;
let offset = 0;
const horz = this._orientation === 'horizontal';
if (nVisible > 0) {
// Compute the adjusted layout space.
let space: number;
if (horz) {
// left += this.widgetOffset;
space = Math.max(0, width - this._fixed);
} else {
// top += this.widgetOffset;
space = Math.max(0, height - this._fixed);
}
// Scale the size hints if they are normalized.
if (this._hasNormedSizes) {
for (const sizer of this._sizers) {
sizer.sizeHint *= space;
}
this._hasNormedSizes = false;
}
// Distribute the layout space to the box sizers.
const delta = BoxEngine.calc(this._sizers, space);
// Account for alignment if there is extra layout space.
if (delta > 0) {
switch (this._alignment) {
case 'start':
break;
case 'center':
extra = 0;
offset = delta / 2;
break;
case 'end':
extra = 0;
offset = delta;
break;
case 'justify':
extra = delta / nVisible;
offset = 0;
break;
default:
throw 'unreachable';
}
}
}
// Layout the items using the computed box sizes.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
const item = this._items[i];
// Fetch the computed size for the widget.
const size = item.isHidden ? 0 : this._sizers[i].size + extra;
this.updateItemPosition(
i,
horz,
horz ? left + offset : left,
horz ? top : top + offset,
height,
width,
size,
);
const fullOffset =
this.widgetOffset +
(this._handles[i].classList.contains('lm-mod-hidden')
? 0
: this._spacing);
if (horz) {
left += size + fullOffset;
} else {
top += size + fullOffset;
}
}
}
protected widgetOffset = 0;
private _fixed = 0;
private _spacing = 4;
private _dirty = false;
private _hasNormedSizes = false;
private _sizers: BoxSizer[] = [];
private _items: LayoutItem[] = [];
private _handles: HTMLDivElement[] = [];
private _box: ElementExt.IBoxSizing | null = null;
private _alignment: SplitLayout.Alignment = 'start';
private _orientation: SplitLayout.Orientation = 'horizontal';
}
/**
* The namespace for the `SplitLayout` class statics.
*/
export namespace SplitLayout {
/**
* A type alias for a split layout orientation.
*/
export type Orientation = 'horizontal' | 'vertical';
/**
* A type alias for a split layout alignment.
*/
export type Alignment = 'start' | 'center' | 'end' | 'justify';
/**
* An options object for initializing a split layout.
*/
export interface IOptions {
/**
* The renderer to use for the split layout.
*/
renderer: IRenderer;
/**
* The orientation of the layout.
*
* The default is `'horizontal'`.
*/
orientation?: Orientation;
/**
* The content alignment of the layout.
*
* The default is `'start'`.
*/
alignment?: Alignment;
/**
* The spacing between items in the layout.
*
* The default is `4`.
*/
spacing?: number;
}
/**
* A renderer for use with a split layout.
*/
export interface IRenderer {
/**
* Create a new handle for use with a split layout.
*
* @returns A new handle element.
*/
createHandle(): HTMLDivElement;
}
/**
* Get the split layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The split layout stretch factor for the widget.
*/
export function getStretch(widget: Widget): number {
return Private.stretchProperty.get(widget);
}
/**
* Set the split layout stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
export function setStretch(widget: Widget, value: number): void {
Private.stretchProperty.set(widget, value);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* The property descriptor for a widget stretch factor.
*/
export const stretchProperty = new AttachedProperty<Widget, number>({
name: 'stretch',
create: () => 0,
coerce: (owner, value) => Math.max(0, Math.floor(value)),
changed: onChildSizingChanged,
});
/**
* Create a new box sizer with the given size hint.
*/
export function createSizer(size: number): BoxSizer {
const sizer = new BoxSizer();
sizer.sizeHint = Math.floor(size);
return sizer;
}
/**
* Create a new split handle node using the given renderer.
*/
export function createHandle(
renderer: SplitLayout.IRenderer,
): HTMLDivElement {
const handle = renderer.createHandle();
handle.style.position = 'absolute';
// Do not use size containment to allow the handle to fill the available space
handle.style.contain = 'style';
return handle;
}
/**
* Compute the average size of an array of box sizers.
*/
export function averageSize(sizers: BoxSizer[]): number {
return sizers.reduce((v, s) => v + s.size, 0) / sizers.length || 0;
}
/**
* Normalize an array of values.
*/
export function normalize(values: number[]): number[] {
const n = values.length;
if (n === 0) {
return [];
}
const sum = values.reduce((a, b) => a + Math.abs(b), 0);
return sum === 0 ? values.map(v => 1 / n) : values.map(v => v / sum);
}
/**
* The change handler for the attached sizing properties.
*/
function onChildSizingChanged(child: Widget): void {
if (child.parent && child.parent.layout instanceof SplitLayout) {
child.parent.fit();
}
}
}

View File

@@ -0,0 +1,500 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type ISignal, Signal } from '../signaling';
import { type Message } from '../messaging';
import { Drag } from '../dragdrop';
import { type IDisposable } from '../disposable';
import { ArrayExt } from '../algorithm';
import { type Widget } from './widget';
import { SplitLayout } from './splitlayout';
import { Panel } from './panel';
/**
* A panel which arranges its widgets into resizable sections.
*
* #### Notes
* This class provides a convenience wrapper around a {@link SplitLayout}.
*/
export class SplitPanel extends Panel {
/**
* Construct a new split panel.
*
* @param options - The options for initializing the split panel.
*/
constructor(options: SplitPanel.IOptions = {}) {
super({ layout: Private.createLayout(options) });
this.addClass('lm-SplitPanel');
}
/**
* Dispose of the resources held by the panel.
*/
dispose(): void {
this._releaseMouse();
super.dispose();
}
/**
* Get the layout orientation for the split panel.
*/
get orientation(): SplitPanel.Orientation {
return (this.layout as SplitLayout).orientation;
}
/**
* Set the layout orientation for the split panel.
*/
set orientation(value: SplitPanel.Orientation) {
(this.layout as SplitLayout).orientation = value;
}
/**
* Get the content alignment for the split panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split panel.
*/
get alignment(): SplitPanel.Alignment {
return (this.layout as SplitLayout).alignment;
}
/**
* Set the content alignment for the split panel.
*
* #### Notes
* This is the alignment of the widgets in the layout direction.
*
* The alignment has no effect if the widgets can expand to fill the
* entire split panel.
*/
set alignment(value: SplitPanel.Alignment) {
(this.layout as SplitLayout).alignment = value;
}
/**
* Get the inter-element spacing for the split panel.
*/
get spacing(): number {
return (this.layout as SplitLayout).spacing;
}
/**
* Set the inter-element spacing for the split panel.
*/
set spacing(value: number) {
(this.layout as SplitLayout).spacing = value;
}
/**
* The renderer used by the split panel.
*/
get renderer(): SplitPanel.IRenderer {
return (this.layout as SplitLayout).renderer;
}
/**
* A signal emitted when a split handle has moved.
*/
get handleMoved(): ISignal<this, void> {
return this._handleMoved;
}
/**
* A read-only array of the split handles in the panel.
*/
get handles(): ReadonlyArray<HTMLDivElement> {
return (this.layout as SplitLayout).handles;
}
/**
* Get the relative sizes of the widgets in the panel.
*
* @returns A new array of the relative sizes of the widgets.
*
* #### Notes
* The returned sizes reflect the sizes of the widgets normalized
* relative to their siblings.
*
* This method **does not** measure the DOM nodes.
*/
relativeSizes(): number[] {
return (this.layout as SplitLayout).relativeSizes();
}
/**
* Set the relative sizes for the widgets in the panel.
*
* @param sizes - The relative sizes for the widgets in the panel.
* @param update - Update the layout after setting relative sizes.
* Default is True.
*
* #### Notes
* Extra values are ignored, too few will yield an undefined layout.
*
* The actual geometry of the DOM nodes is updated asynchronously.
*/
setRelativeSizes(sizes: number[], update = true): void {
(this.layout as SplitLayout).setRelativeSizes(sizes, update);
}
/**
* Handle the DOM events for the split panel.
*
* @param event - The DOM event sent to the panel.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the panel's DOM node. It should
* not be called directly by user code.
*/
handleEvent(event: Event): void {
switch (event.type) {
case 'pointerdown':
this._evtPointerDown(event as PointerEvent);
break;
case 'pointermove':
this._evtPointerMove(event as PointerEvent);
break;
case 'pointerup':
this._evtPointerUp(event as PointerEvent);
break;
case 'keydown':
this._evtKeyDown(event as KeyboardEvent);
break;
case 'contextmenu':
event.preventDefault();
event.stopPropagation();
break;
}
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
protected onBeforeAttach(msg: Message): void {
this.node.addEventListener('pointerdown', this);
}
/**
* A message handler invoked on an `'after-detach'` message.
*/
protected onAfterDetach(msg: Message): void {
this.node.removeEventListener('pointerdown', this);
this._releaseMouse();
}
/**
* A message handler invoked on a `'child-added'` message.
*/
protected onChildAdded(msg: Widget.ChildMessage): void {
msg.child.addClass('lm-SplitPanel-child');
this._releaseMouse();
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
protected onChildRemoved(msg: Widget.ChildMessage): void {
msg.child.removeClass('lm-SplitPanel-child');
this._releaseMouse();
}
/**
* Handle the `'keydown'` event for the split panel.
*/
private _evtKeyDown(event: KeyboardEvent): void {
// Stop input events during drag.
if (this._pressData) {
event.preventDefault();
event.stopPropagation();
}
// Release the mouse if `Escape` is pressed.
if (event.keyCode === 27) {
this._releaseMouse();
}
}
/**
* Handle the `'pointerdown'` event for the split panel.
*/
private _evtPointerDown(event: PointerEvent): void {
// Do nothing if the primary button is not pressed.
if (event.button !== 0) {
return;
}
// Find the handle which contains the target, if any.
const layout = this.layout as SplitLayout;
const index = ArrayExt.findFirstIndex(layout.handles, handle =>
handle.contains(event.target as HTMLElement),
);
// Bail early if the mouse press was not on a handle.
if (index === -1) {
return;
}
// Stop the event when a split handle is pressed.
event.preventDefault();
event.stopPropagation();
// Add the extra document listeners.
document.addEventListener('pointerup', this, true);
document.addEventListener('pointermove', this, true);
document.addEventListener('keydown', this, true);
document.addEventListener('contextmenu', this, true);
// Compute the offset delta for the handle press.
let delta: number;
const handle = layout.handles[index];
const rect = handle.getBoundingClientRect();
if (layout.orientation === 'horizontal') {
delta = event.clientX - rect.left;
} else {
delta = event.clientY - rect.top;
}
// Override the cursor and store the press data.
const style = window.getComputedStyle(handle);
const override = Drag.overrideCursor(style.cursor!);
this._pressData = { index, delta, override };
}
/**
* Handle the `'pointermove'` event for the split panel.
*/
private _evtPointerMove(event: PointerEvent): void {
// Stop the event when dragging a split handle.
event.preventDefault();
event.stopPropagation();
// Compute the desired offset position for the handle.
let pos: number;
const layout = this.layout as SplitLayout;
const rect = this.node.getBoundingClientRect();
if (layout.orientation === 'horizontal') {
pos = event.clientX - rect.left - this._pressData!.delta;
} else {
pos = event.clientY - rect.top - this._pressData!.delta;
}
// Move the handle as close to the desired position as possible.
layout.moveHandle(this._pressData!.index, pos);
}
/**
* Handle the `'pointerup'` event for the split panel.
*/
private _evtPointerUp(event: PointerEvent): void {
// Do nothing if the primary button is not released.
if (event.button !== 0) {
return;
}
// Stop the event when releasing a handle.
event.preventDefault();
event.stopPropagation();
// Finalize the mouse release.
this._releaseMouse();
}
/**
* Release the mouse grab for the split panel.
*/
private _releaseMouse(): void {
// Bail early if no drag is in progress.
if (!this._pressData) {
return;
}
// Clear the override cursor.
this._pressData.override.dispose();
this._pressData = null;
// Emit the handle moved signal.
this._handleMoved.emit();
// Remove the extra document listeners.
document.removeEventListener('keydown', this, true);
document.removeEventListener('pointerup', this, true);
document.removeEventListener('pointermove', this, true);
document.removeEventListener('contextmenu', this, true);
}
private _handleMoved = new Signal<any, void>(this);
private _pressData: Private.IPressData | null = null;
}
/**
* The namespace for the `SplitPanel` class statics.
*/
export namespace SplitPanel {
/**
* A type alias for a split panel orientation.
*/
export type Orientation = SplitLayout.Orientation;
/**
* A type alias for a split panel alignment.
*/
export type Alignment = SplitLayout.Alignment;
/**
* A type alias for a split panel renderer.
*/
export type IRenderer = SplitLayout.IRenderer;
/**
* An options object for initializing a split panel.
*/
export interface IOptions {
/**
* The renderer to use for the split panel.
*
* The default is a shared renderer instance.
*/
renderer?: IRenderer;
/**
* The layout orientation of the panel.
*
* The default is `'horizontal'`.
*/
orientation?: Orientation;
/**
* The content alignment of the panel.
*
* The default is `'start'`.
*/
alignment?: Alignment;
/**
* The spacing between items in the panel.
*
* The default is `4`.
*/
spacing?: number;
/**
* The split layout to use for the split panel.
*
* If this is provided, the other options are ignored.
*
* The default is a new `SplitLayout`.
*/
layout?: SplitLayout;
}
/**
* The default implementation of `IRenderer`.
*/
export class Renderer implements IRenderer {
/**
* Create a new handle for use with a split panel.
*
* @returns A new handle element for a split panel.
*/
createHandle(): HTMLDivElement {
const handle = document.createElement('div');
handle.className = 'lm-SplitPanel-handle';
return handle;
}
}
/**
* The default `Renderer` instance.
*/
export const defaultRenderer = new Renderer();
/**
* Get the split panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @returns The split panel stretch factor for the widget.
*/
export function getStretch(widget: Widget): number {
return SplitLayout.getStretch(widget);
}
/**
* Set the split panel stretch factor for the given widget.
*
* @param widget - The widget of interest.
*
* @param value - The value for the stretch factor.
*/
export function setStretch(widget: Widget, value: number): void {
SplitLayout.setStretch(widget, value);
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* An object which holds mouse press data.
*/
export interface IPressData {
/**
* The index of the pressed handle.
*/
index: number;
/**
* The offset of the press in handle coordinates.
*/
delta: number;
/**
* The disposable which will clear the override cursor.
*/
override: IDisposable;
}
/**
* Create a split layout for the given panel options.
*/
export function createLayout(options: SplitPanel.IOptions): SplitLayout {
return (
options.layout ||
new SplitLayout({
renderer: options.renderer || SplitPanel.defaultRenderer,
orientation: options.orientation,
alignment: options.alignment,
spacing: options.spacing,
})
);
}
}

View File

@@ -0,0 +1,401 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type Message, MessageLoop } from '../messaging';
import { ElementExt } from '../domutils';
import { ArrayExt } from '../algorithm';
import { Widget } from './widget';
import { PanelLayout } from './panellayout';
import { type Layout, LayoutItem } from './layout';
/**
* A layout where visible widgets are stacked atop one another.
*
* #### Notes
* The Z-order of the visible widgets follows their layout order.
*/
export class StackedLayout extends PanelLayout {
constructor(options: StackedLayout.IOptions = {}) {
super(options);
this._hiddenMode =
options.hiddenMode !== undefined
? options.hiddenMode
: Widget.HiddenMode.Display;
}
/**
* The method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
get hiddenMode(): Widget.HiddenMode {
return this._hiddenMode;
}
/**
* Set the method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
set hiddenMode(v: Widget.HiddenMode) {
if (this._hiddenMode === v) {
return;
}
this._hiddenMode = v;
if (this.widgets.length > 1) {
this.widgets.forEach(w => {
w.hiddenMode = this._hiddenMode;
});
}
}
/**
* Dispose of the resources held by the layout.
*/
dispose(): void {
// Dispose of the layout items.
for (const item of this._items) {
item.dispose();
}
// Clear the layout state.
this._box = null;
this._items.length = 0;
// Dispose of the rest of the layout.
super.dispose();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected attachWidget(index: number, widget: Widget): void {
// Using transform create an additional layer in the pixel pipeline
// to limit the number of layer, it is set only if there is more than one widget.
if (
this._hiddenMode === Widget.HiddenMode.Scale &&
this._items.length > 0
) {
if (this._items.length === 1) {
this.widgets[0].hiddenMode = Widget.HiddenMode.Scale;
}
widget.hiddenMode = Widget.HiddenMode.Scale;
} else {
widget.hiddenMode = Widget.HiddenMode.Display;
}
// Create and add a new layout item for the widget.
ArrayExt.insert(this._items, index, new LayoutItem(widget));
// Send a `'before-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
}
// Add the widget's node to the parent.
this.parent!.node.appendChild(widget.node);
// Send an `'after-attach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
}
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* Move a widget in the parent's DOM node.
*
* @param fromIndex - The previous index of the widget in the layout.
*
* @param toIndex - The current index of the widget in the layout.
*
* @param widget - The widget to move in the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected moveWidget(
fromIndex: number,
toIndex: number,
widget: Widget,
): void {
// Move the layout item for the widget.
ArrayExt.move(this._items, fromIndex, toIndex);
// Post an update request for the parent widget.
this.parent!.update();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
protected detachWidget(index: number, widget: Widget): void {
// Remove the layout item for the widget.
const item = ArrayExt.removeAt(this._items, index);
// Send a `'before-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
}
// Remove the widget's node from the parent.
this.parent!.node.removeChild(widget.node);
// Send an `'after-detach'` message if the parent is attached.
if (this.parent!.isAttached) {
MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
}
// Reset the z-index for the widget.
item!.widget.node.style.zIndex = '';
// Reset the hidden mode for the widget.
if (this._hiddenMode === Widget.HiddenMode.Scale) {
widget.hiddenMode = Widget.HiddenMode.Display;
// Reset the hidden mode for the first widget if necessary.
if (this._items.length === 1) {
this._items[0].widget.hiddenMode = Widget.HiddenMode.Display;
}
}
// Dispose of the layout item.
item!.dispose();
// Post a fit request for the parent widget.
this.parent!.fit();
}
/**
* A message handler invoked on a `'before-show'` message.
*/
protected onBeforeShow(msg: Message): void {
super.onBeforeShow(msg);
this.parent!.update();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
protected onBeforeAttach(msg: Message): void {
super.onBeforeAttach(msg);
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
protected onChildShown(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
protected onChildHidden(msg: Widget.ChildMessage): void {
this.parent!.fit();
}
/**
* A message handler invoked on a `'resize'` message.
*/
protected onResize(msg: Widget.ResizeMessage): void {
if (this.parent!.isVisible) {
this._update(msg.width, msg.height);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
protected onUpdateRequest(msg: Message): void {
if (this.parent!.isVisible) {
this._update(-1, -1);
}
}
/**
* A message handler invoked on a `'fit-request'` message.
*/
protected onFitRequest(msg: Message): void {
if (this.parent!.isAttached) {
this._fit();
}
}
/**
* Fit the layout to the total size required by the widgets.
*/
private _fit(): void {
// Set up the computed minimum size.
let minW = 0;
let minH = 0;
// Update the computed minimum size.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
const item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Update the size limits for the item.
item.fit();
// Update the computed minimum size.
minW = Math.max(minW, item.minWidth);
minH = Math.max(minH, item.minHeight);
}
// Update the box sizing and add it to the computed min size.
const box = (this._box = ElementExt.boxSizing(this.parent!.node));
minW += box.horizontalSum;
minH += box.verticalSum;
// Update the parent's min size constraints.
const { style } = this.parent!.node;
style.minWidth = `${minW}px`;
style.minHeight = `${minH}px`;
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent!.parent) {
MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
}
}
/**
* Update the layout position and size of the widgets.
*
* The parent offset dimensions should be `-1` if unknown.
*/
private _update(offsetWidth: number, offsetHeight: number): void {
// Clear the dirty flag to indicate the update occurred.
this._dirty = false;
// Compute the visible item count.
let nVisible = 0;
for (let i = 0, n = this._items.length; i < n; ++i) {
nVisible += +!this._items[i].isHidden;
}
// Bail early if there are no visible items to layout.
if (nVisible === 0) {
return;
}
// Measure the parent if the offset dimensions are unknown.
if (offsetWidth < 0) {
offsetWidth = this.parent!.node.offsetWidth;
}
if (offsetHeight < 0) {
offsetHeight = this.parent!.node.offsetHeight;
}
// Ensure the parent box sizing data is computed.
if (!this._box) {
this._box = ElementExt.boxSizing(this.parent!.node);
}
// Compute the actual layout bounds adjusted for border and padding.
const top = this._box.paddingTop;
const left = this._box.paddingLeft;
const width = offsetWidth - this._box.horizontalSum;
const height = offsetHeight - this._box.verticalSum;
// Update the widget stacking order and layout geometry.
for (let i = 0, n = this._items.length; i < n; ++i) {
// Fetch the item.
const item = this._items[i];
// Ignore hidden items.
if (item.isHidden) {
continue;
}
// Set the z-index for the widget.
item.widget.node.style.zIndex = `${i}`;
// Update the item geometry.
item.update(left, top, width, height);
}
}
private _dirty = false;
private _items: LayoutItem[] = [];
private _box: ElementExt.IBoxSizing | null = null;
private _hiddenMode: Widget.HiddenMode;
}
/**
* The namespace for the `StackedLayout` class statics.
*/
export namespace StackedLayout {
/**
* An options object for initializing a stacked layout.
*/
export interface IOptions extends Layout.IOptions {
/**
* The method for hiding widgets.
*
* The default is `Widget.HiddenMode.Display`.
*/
hiddenMode?: Widget.HiddenMode;
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type ISignal, Signal } from '../signaling';
import { type Widget } from './widget';
import { StackedLayout } from './stackedlayout';
import { Panel } from './panel';
/**
* A panel where visible widgets are stacked atop one another.
*
* #### Notes
* This class provides a convenience wrapper around a {@link StackedLayout}.
*/
export class StackedPanel extends Panel {
/**
* Construct a new stacked panel.
*
* @param options - The options for initializing the panel.
*/
constructor(options: StackedPanel.IOptions = {}) {
super({ layout: Private.createLayout(options) });
this.addClass('lm-StackedPanel');
}
/**
* The method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
get hiddenMode(): Widget.HiddenMode {
return (this.layout as StackedLayout).hiddenMode;
}
/**
* Set the method for hiding widgets.
*
* #### Notes
* If there is only one child widget, `Display` hiding mode will be used
* regardless of this setting.
*/
set hiddenMode(v: Widget.HiddenMode) {
(this.layout as StackedLayout).hiddenMode = v;
}
/**
* A signal emitted when a widget is removed from a stacked panel.
*/
get widgetRemoved(): ISignal<this, Widget> {
return this._widgetRemoved;
}
/**
* A message handler invoked on a `'child-added'` message.
*/
protected onChildAdded(msg: Widget.ChildMessage): void {
msg.child.addClass('lm-StackedPanel-child');
}
/**
* A message handler invoked on a `'child-removed'` message.
*/
protected onChildRemoved(msg: Widget.ChildMessage): void {
msg.child.removeClass('lm-StackedPanel-child');
this._widgetRemoved.emit(msg.child);
}
private _widgetRemoved = new Signal<this, Widget>(this);
}
/**
* The namespace for the `StackedPanel` class statics.
*/
export namespace StackedPanel {
/**
* An options object for creating a stacked panel.
*/
export interface IOptions {
/**
* The stacked layout to use for the stacked panel.
*
* The default is a new `StackedLayout`.
*/
layout?: StackedLayout;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* Create a stacked layout for the given panel options.
*/
export function createLayout(options: StackedPanel.IOptions): StackedLayout {
return options.layout || new StackedLayout();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,532 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type ISignal, Signal } from '../signaling';
import { MessageLoop } from '../messaging';
import { Platform } from '../domutils';
import { Widget } from './widget';
import { TabBar } from './tabbar';
import { StackedPanel } from './stackedpanel';
import { BoxLayout } from './boxlayout';
/**
* A widget which combines a `TabBar` and a `StackedPanel`.
*
* #### Notes
* This is a simple panel which handles the common case of a tab bar
* placed next to a content area. The selected tab controls the widget
* which is shown in the content area.
*
* For use cases which require more control than is provided by this
* panel, the `TabBar` widget may be used independently.
*/
export class TabPanel extends Widget {
/**
* Construct a new tab panel.
*
* @param options - The options for initializing the tab panel.
*/
constructor(options: TabPanel.IOptions = {}) {
super();
this.addClass('lm-TabPanel');
// Create the tab bar and stacked panel.
this.tabBar = new TabBar<Widget>(options);
this.tabBar.addClass('lm-TabPanel-tabBar');
this.stackedPanel = new StackedPanel();
this.stackedPanel.addClass('lm-TabPanel-stackedPanel');
// Connect the tab bar signal handlers.
this.tabBar.tabMoved.connect(this._onTabMoved, this);
this.tabBar.currentChanged.connect(this._onCurrentChanged, this);
this.tabBar.tabCloseRequested.connect(this._onTabCloseRequested, this);
this.tabBar.tabActivateRequested.connect(
this._onTabActivateRequested,
this,
);
this.tabBar.addRequested.connect(this._onTabAddRequested, this);
// Connect the stacked panel signal handlers.
this.stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
// Get the data related to the placement.
this._tabPlacement = options.tabPlacement || 'top';
const direction = Private.directionFromPlacement(this._tabPlacement);
const orientation = Private.orientationFromPlacement(this._tabPlacement);
// Configure the tab bar for the placement.
this.tabBar.orientation = orientation;
this.tabBar.dataset.placement = this._tabPlacement;
// Create the box layout.
const layout = new BoxLayout({ direction, spacing: 0 });
// Set the stretch factors for the child widgets.
BoxLayout.setStretch(this.tabBar, 0);
BoxLayout.setStretch(this.stackedPanel, 1);
// Add the child widgets to the layout.
layout.addWidget(this.tabBar);
layout.addWidget(this.stackedPanel);
// Install the layout on the tab panel.
this.layout = layout;
}
/**
* A signal emitted when the current tab is changed.
*
* #### Notes
* This signal is emitted when the currently selected tab is changed
* either through user or programmatic interaction.
*
* Notably, this signal is not emitted when the index of the current
* tab changes due to tabs being inserted, removed, or moved. It is
* only emitted when the actual current tab node is changed.
*/
get currentChanged(): ISignal<this, TabPanel.ICurrentChangedArgs> {
return this._currentChanged;
}
/**
* Get the index of the currently selected tab.
*
* #### Notes
* This will be `-1` if no tab is selected.
*/
get currentIndex(): number {
return this.tabBar.currentIndex;
}
/**
* Set the index of the currently selected tab.
*
* #### Notes
* If the index is out of range, it will be set to `-1`.
*/
set currentIndex(value: number) {
this.tabBar.currentIndex = value;
}
/**
* Get the currently selected widget.
*
* #### Notes
* This will be `null` if there is no selected tab.
*/
get currentWidget(): Widget | null {
const title = this.tabBar.currentTitle;
return title ? title.owner : null;
}
/**
* Set the currently selected widget.
*
* #### Notes
* If the widget is not in the panel, it will be set to `null`.
*/
set currentWidget(value: Widget | null) {
this.tabBar.currentTitle = value ? value.title : null;
}
/**
* Get the whether the tabs are movable by the user.
*
* #### Notes
* Tabs can always be moved programmatically.
*/
get tabsMovable(): boolean {
return this.tabBar.tabsMovable;
}
/**
* Set the whether the tabs are movable by the user.
*
* #### Notes
* Tabs can always be moved programmatically.
*/
set tabsMovable(value: boolean) {
this.tabBar.tabsMovable = value;
}
/**
* Get the whether the add button is enabled.
*
*/
get addButtonEnabled(): boolean {
return this.tabBar.addButtonEnabled;
}
/**
* Set the whether the add button is enabled.
*
*/
set addButtonEnabled(value: boolean) {
this.tabBar.addButtonEnabled = value;
}
/**
* Get the tab placement for the tab panel.
*
* #### Notes
* This controls the position of the tab bar relative to the content.
*/
get tabPlacement(): TabPanel.TabPlacement {
return this._tabPlacement;
}
/**
* Set the tab placement for the tab panel.
*
* #### Notes
* This controls the position of the tab bar relative to the content.
*/
set tabPlacement(value: TabPanel.TabPlacement) {
// Bail if the placement does not change.
if (this._tabPlacement === value) {
return;
}
// Update the internal value.
this._tabPlacement = value;
// Get the values related to the placement.
const direction = Private.directionFromPlacement(value);
const orientation = Private.orientationFromPlacement(value);
// Configure the tab bar for the placement.
this.tabBar.orientation = orientation;
this.tabBar.dataset.placement = value;
// Update the layout direction.
(this.layout as BoxLayout).direction = direction;
}
/**
* A signal emitted when the add button on a tab bar is clicked.
*
*/
get addRequested(): ISignal<this, TabBar<Widget>> {
return this._addRequested;
}
/**
* The tab bar used by the tab panel.
*
* #### Notes
* Modifying the tab bar directly can lead to undefined behavior.
*/
readonly tabBar: TabBar<Widget>;
/**
* The stacked panel used by the tab panel.
*
* #### Notes
* Modifying the panel directly can lead to undefined behavior.
*/
readonly stackedPanel: StackedPanel;
/**
* A read-only array of the widgets in the panel.
*/
get widgets(): ReadonlyArray<Widget> {
return this.stackedPanel.widgets;
}
/**
* Add a widget to the end of the tab panel.
*
* @param widget - The widget to add to the tab panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*
* The widget's `title` is used to populate the tab.
*/
addWidget(widget: Widget): void {
this.insertWidget(this.widgets.length, widget);
}
/**
* Insert a widget into the tab panel at a specified index.
*
* @param index - The index at which to insert the widget.
*
* @param widget - The widget to insert into to the tab panel.
*
* #### Notes
* If the widget is already contained in the panel, it will be moved.
*
* The widget's `title` is used to populate the tab.
*/
insertWidget(index: number, widget: Widget): void {
if (widget !== this.currentWidget) {
widget.hide();
}
this.stackedPanel.insertWidget(index, widget);
this.tabBar.insertTab(index, widget.title);
widget.node.setAttribute('role', 'tabpanel');
const { renderer } = this.tabBar;
if (renderer instanceof TabBar.Renderer) {
const tabId = renderer.createTabKey({
title: widget.title,
current: false,
zIndex: 0,
});
widget.node.setAttribute('aria-labelledby', tabId);
}
}
/**
* Handle the `currentChanged` signal from the tab bar.
*/
private _onCurrentChanged(
sender: TabBar<Widget>,
args: TabBar.ICurrentChangedArgs<Widget>,
): void {
// Extract the previous and current title from the args.
const { previousIndex, previousTitle, currentIndex, currentTitle } = args;
// Extract the widgets from the titles.
const previousWidget = previousTitle ? previousTitle.owner : null;
const currentWidget = currentTitle ? currentTitle.owner : null;
// Hide the previous widget.
if (previousWidget) {
previousWidget.hide();
}
// Show the current widget.
if (currentWidget) {
currentWidget.show();
}
// Emit the `currentChanged` signal for the tab panel.
this._currentChanged.emit({
previousIndex,
previousWidget,
currentIndex,
currentWidget,
});
// Flush the message loop on IE and Edge to prevent flicker.
if (Platform.IS_EDGE || Platform.IS_IE) {
MessageLoop.flush();
}
}
/**
* Handle the `tabAddRequested` signal from the tab bar.
*/
private _onTabAddRequested(sender: TabBar<Widget>, args: void): void {
this._addRequested.emit(sender);
}
/**
* Handle the `tabActivateRequested` signal from the tab bar.
*/
private _onTabActivateRequested(
sender: TabBar<Widget>,
args: TabBar.ITabActivateRequestedArgs<Widget>,
): void {
args.title.owner.activate();
}
/**
* Handle the `tabCloseRequested` signal from the tab bar.
*/
private _onTabCloseRequested(
sender: TabBar<Widget>,
args: TabBar.ITabCloseRequestedArgs<Widget>,
): void {
args.title.owner.close();
}
/**
* Handle the `tabMoved` signal from the tab bar.
*/
private _onTabMoved(
sender: TabBar<Widget>,
args: TabBar.ITabMovedArgs<Widget>,
): void {
this.stackedPanel.insertWidget(args.toIndex, args.title.owner);
}
/**
* Handle the `widgetRemoved` signal from the stacked panel.
*/
private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
widget.node.removeAttribute('role');
widget.node.removeAttribute('aria-labelledby');
this.tabBar.removeTab(widget.title);
}
private _tabPlacement: TabPanel.TabPlacement;
private _currentChanged = new Signal<this, TabPanel.ICurrentChangedArgs>(
this,
);
private _addRequested = new Signal<this, TabBar<Widget>>(this);
}
/**
* The namespace for the `TabPanel` class statics.
*/
export namespace TabPanel {
/**
* A type alias for tab placement in a tab bar.
*/
export type TabPlacement =
| /**
* The tabs are placed as a row above the content.
*/
'top'
/**
* The tabs are placed as a column to the left of the content.
*/
| 'left'
/**
* The tabs are placed as a column to the right of the content.
*/
| 'right'
/**
* The tabs are placed as a row below the content.
*/
| 'bottom';
/**
* An options object for initializing a tab panel.
*/
export interface IOptions {
/**
* The document to use with the tab panel.
*
* The default is the global `document` instance.
*/
document?: Document | ShadowRoot;
/**
* Whether the tabs are movable by the user.
*
* The default is `false`.
*/
tabsMovable?: boolean;
/**
* Whether the button to add new tabs is enabled.
*
* The default is `false`.
*/
addButtonEnabled?: boolean;
/**
* The placement of the tab bar relative to the content.
*
* The default is `'top'`.
*/
tabPlacement?: TabPlacement;
/**
* The renderer for the panel's tab bar.
*
* The default is a shared renderer instance.
*/
renderer?: TabBar.IRenderer<Widget>;
}
/**
* The arguments object for the `currentChanged` signal.
*/
export interface ICurrentChangedArgs {
/**
* The previously selected index.
*/
previousIndex: number;
/**
* The previously selected widget.
*/
previousWidget: Widget | null;
/**
* The currently selected index.
*/
currentIndex: number;
/**
* The currently selected widget.
*/
currentWidget: Widget | null;
}
}
/**
* The namespace for the module implementation details.
*/
namespace Private {
/**
* Convert a tab placement to tab bar orientation.
*/
export function orientationFromPlacement(
plc: TabPanel.TabPlacement,
): TabBar.Orientation {
return placementToOrientationMap[plc];
}
/**
* Convert a tab placement to a box layout direction.
*/
export function directionFromPlacement(
plc: TabPanel.TabPlacement,
): BoxLayout.Direction {
return placementToDirectionMap[plc];
}
/**
* A mapping of tab placement to tab bar orientation.
*/
const placementToOrientationMap: { [key: string]: TabBar.Orientation } = {
top: 'horizontal',
left: 'vertical',
right: 'vertical',
bottom: 'horizontal',
};
/**
* A mapping of tab placement to box layout direction.
*/
const placementToDirectionMap: { [key: string]: BoxLayout.Direction } = {
top: 'top-to-bottom',
left: 'left-to-right',
right: 'right-to-left',
bottom: 'bottom-to-top',
};
}

View File

@@ -0,0 +1,405 @@
/*
* 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.
*/
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/
import { type VirtualElement } from '../virtualdom';
import { type ISignal, Signal } from '../signaling';
import { type IDisposable } from '../disposable';
/**
* An object which holds data related to an object's title.
*
* #### Notes
* A title object is intended to hold the data necessary to display a
* header for a particular object. A common example is the `TabPanel`,
* which uses the widget title to populate the tab for a child widget.
*
* It is the responsibility of the owner to call the title disposal.
*/
export class Title<T> implements IDisposable {
/**
* Construct a new title.
*
* @param options - The options for initializing the title.
*/
constructor(options: Title.IOptions<T>) {
this.owner = options.owner;
if (options.label !== undefined) {
this._label = options.label;
}
if (options.mnemonic !== undefined) {
this._mnemonic = options.mnemonic;
}
if (options.icon !== undefined) {
this._icon = options.icon;
}
if (options.iconClass !== undefined) {
this._iconClass = options.iconClass;
}
if (options.iconLabel !== undefined) {
this._iconLabel = options.iconLabel;
}
if (options.caption !== undefined) {
this._caption = options.caption;
}
if (options.className !== undefined) {
this._className = options.className;
}
if (options.closable !== undefined) {
this._closable = options.closable;
}
this._dataset = options.dataset || {};
}
/**
* A signal emitted when the state of the title changes.
*/
get changed(): ISignal<this, void> {
return this._changed;
}
/**
* The object which owns the title.
*/
readonly owner: T;
/**
* Get the label for the title.
*
* #### Notes
* The default value is an empty string.
*/
get label(): string {
return this._label;
}
/**
* Set the label for the title.
*/
set label(value: string) {
if (this._label === value) {
return;
}
this._label = value;
this._changed.emit(undefined);
}
/**
* Get the mnemonic index for the title.
*
* #### Notes
* The default value is `-1`.
*/
get mnemonic(): number {
return this._mnemonic;
}
/**
* Set the mnemonic index for the title.
*/
set mnemonic(value: number) {
if (this._mnemonic === value) {
return;
}
this._mnemonic = value;
this._changed.emit(undefined);
}
/**
* Get the icon renderer for the title.
*
* #### Notes
* The default value is undefined.
*/
get icon(): VirtualElement.IRenderer | undefined {
return this._icon;
}
/**
* Set the icon renderer for the title.
*
* #### Notes
* A renderer is an object that supplies a render and unrender function.
*/
set icon(value: VirtualElement.IRenderer | undefined) {
if (this._icon === value) {
return;
}
this._icon = value;
this._changed.emit(undefined);
}
/**
* Get the icon class name for the title.
*
* #### Notes
* The default value is an empty string.
*/
get iconClass(): string {
return this._iconClass;
}
/**
* Set the icon class name for the title.
*
* #### Notes
* Multiple class names can be separated with whitespace.
*/
set iconClass(value: string) {
if (this._iconClass === value) {
return;
}
this._iconClass = value;
this._changed.emit(undefined);
}
/**
* Get the icon label for the title.
*
* #### Notes
* The default value is an empty string.
*/
get iconLabel(): string {
return this._iconLabel;
}
/**
* Set the icon label for the title.
*
* #### Notes
* Multiple class names can be separated with whitespace.
*/
set iconLabel(value: string) {
if (this._iconLabel === value) {
return;
}
this._iconLabel = value;
this._changed.emit(undefined);
}
/**
* Get the caption for the title.
*
* #### Notes
* The default value is an empty string.
*/
get caption(): string {
return this._caption;
}
/**
* Set the caption for the title.
*/
set caption(value: string) {
if (this._caption === value) {
return;
}
this._caption = value;
this._changed.emit(undefined);
}
/**
* Get the extra class name for the title.
*
* #### Notes
* The default value is an empty string.
*/
get className(): string {
return this._className;
}
/**
* Set the extra class name for the title.
*
* #### Notes
* Multiple class names can be separated with whitespace.
*/
set className(value: string) {
if (this._className === value) {
return;
}
this._className = value;
this._changed.emit(undefined);
}
/**
* Get the closable state for the title.
*
* #### Notes
* The default value is `false`.
*/
get closable(): boolean {
return this._closable;
}
/**
* Set the closable state for the title.
*
* #### Notes
* This controls the presence of a close icon when applicable.
*/
set closable(value: boolean) {
if (this._closable === value) {
return;
}
this._closable = value;
this._changed.emit(undefined);
}
/**
* Get the dataset for the title.
*
* #### Notes
* The default value is an empty dataset.
*/
get dataset(): Title.Dataset {
return this._dataset;
}
/**
* Set the dataset for the title.
*
* #### Notes
* This controls the data attributes when applicable.
*/
set dataset(value: Title.Dataset) {
if (this._dataset === value) {
return;
}
this._dataset = value;
this._changed.emit(undefined);
}
/**
* Test whether the title has been disposed.
*/
get isDisposed(): boolean {
return this._isDisposed;
}
/**
* Dispose of the resources held by the title.
*
* #### Notes
* It is the responsibility of the owner to call the title disposal.
*/
dispose(): void {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
Signal.clearData(this);
}
private _label = '';
private _caption = '';
private _mnemonic = -1;
private _icon: VirtualElement.IRenderer | undefined = undefined;
private _iconClass = '';
private _iconLabel = '';
private _className = '';
private _closable = false;
private _dataset: Title.Dataset;
private _changed = new Signal<this, void>(this);
private _isDisposed = false;
}
/**
* The namespace for the `Title` class statics.
*/
export namespace Title {
/**
* A type alias for a simple immutable string dataset.
*/
export interface Dataset {
readonly [key: string]: string;
}
/**
* An options object for initializing a title.
*/
export interface IOptions<T> {
/**
* The object which owns the title.
*/
owner: T;
/**
* The label for the title.
*/
label?: string;
/**
* The mnemonic index for the title.
*/
mnemonic?: number;
/**
* The icon renderer for the title.
*/
icon?: VirtualElement.IRenderer;
/**
* The icon class name for the title.
*/
iconClass?: string;
/**
* The icon label for the title.
*/
iconLabel?: string;
/**
* The caption for the title.
*/
caption?: string;
/**
* The extra class name for the title.
*/
className?: string;
/**
* The closable state for the title.
*/
closable?: boolean;
/**
* The dataset for the title.
*/
dataset?: Dataset;
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.
*/
/*
* Copyright (c) Jupyter Development Team.
* Distributed under the terms of the Modified BSD License.
*/
export namespace Utils {
/**
* Clamp a dimension value to an integer >= 0.
*/
export function clampDimension(value: number): number {
return Math.max(0, Math.floor(value));
}
}
export default Utils;

File diff suppressed because it is too large Load Diff