feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
import {
|
||||
LabelService,
|
||||
OpenerService,
|
||||
type URI,
|
||||
useIDEService,
|
||||
} from '@coze-project-ide/core';
|
||||
|
||||
import { codicon } from '../../utils';
|
||||
import { type ActivityBarItem, LayoutPanelType } from '../../types/view';
|
||||
import { HoverService } from '../../services/hover-service';
|
||||
import { useCurrentWidgetFromArea } from '../../hooks';
|
||||
import { useStyling } from './styles';
|
||||
|
||||
interface ActivityBarProps {
|
||||
list: ActivityBarItem[];
|
||||
currentUri?: URI;
|
||||
setCurrentUri: (uri: URI) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* activitybar 有两种状态
|
||||
* - 选中态 select 同时高亮和左侧有蓝色竖线
|
||||
* - 激活态 active 仅高亮
|
||||
*/
|
||||
export const ActivityBar: React.FC<ActivityBarProps> = ({
|
||||
list,
|
||||
currentUri,
|
||||
setCurrentUri,
|
||||
}) => {
|
||||
const labelService = useIDEService<LabelService>(LabelService);
|
||||
const hoverService = useIDEService<HoverService>(HoverService);
|
||||
const openerService = useIDEService<OpenerService>(OpenerService);
|
||||
|
||||
const mainPanelUri = useCurrentWidgetFromArea(
|
||||
LayoutPanelType.MAIN_PANEL,
|
||||
)?.uri;
|
||||
|
||||
const renderIcon = (item: ActivityBarItem) => {
|
||||
const icon = labelService.getIcon(item.uri);
|
||||
if (typeof icon !== 'string') {
|
||||
return icon;
|
||||
}
|
||||
return <i className={codicon(icon)} />;
|
||||
};
|
||||
|
||||
const handleItemClick = async (
|
||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
||||
item: ActivityBarItem,
|
||||
) => {
|
||||
if (item?.onClick) {
|
||||
item.onClick(e);
|
||||
} else if (item.position === 'top') {
|
||||
setCurrentUri(item.uri);
|
||||
} else {
|
||||
openerService.open(item.uri);
|
||||
}
|
||||
hoverService.cancelHover();
|
||||
};
|
||||
|
||||
const handleTooltip = useCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement, MouseEvent>, content?: string) => {
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
hoverService.requestHover({
|
||||
content,
|
||||
target: e.currentTarget,
|
||||
position: 'right',
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const renderListItem = (item: ActivityBarItem) => {
|
||||
const title = labelService.getName(item.uri);
|
||||
const isSelect = currentUri && item.uri.isEqualOrParent(currentUri);
|
||||
const isActive = mainPanelUri && item.uri.isEqualOrParent(mainPanelUri);
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className={clsx(
|
||||
'item-container',
|
||||
isSelect && 'selected',
|
||||
isActive && 'active',
|
||||
)}
|
||||
onClick={e => handleItemClick(e, item)}
|
||||
onMouseEnter={e => !isSelect && handleTooltip(e, item.tooltip)}
|
||||
>
|
||||
{renderIcon(item)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useStyling();
|
||||
|
||||
return (
|
||||
<div className="activity-bar-widget-container">
|
||||
<div className="top-container">
|
||||
{list
|
||||
.filter(item => item.position === 'top')
|
||||
.map(item => renderListItem(item))}
|
||||
</div>
|
||||
<div className="bottom-container">
|
||||
{list
|
||||
.filter(item => item.position === 'bottom')
|
||||
.map(item => renderListItem(item))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useStyling as useStylingCore } from '@coze-project-ide/core';
|
||||
|
||||
export const useStyling = () => {
|
||||
useStylingCore(
|
||||
'flowide-activity-bar-widget',
|
||||
(_, { getColor }) => `
|
||||
.activity-bar-widget-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
.top-container, .bottom-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
color: ${getColor('flowide.color.base.text.2')};
|
||||
}
|
||||
.item-container.active {
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
}
|
||||
.item-container.selected {
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
}
|
||||
.item-container.selected::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
}
|
||||
.item-container:hover {
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
}
|
||||
|
||||
.item-container > i {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
mask-repeat: no-repeat;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
mask-size: 24px;
|
||||
-webkit-mask-size: 24px;
|
||||
mask-position: 50% 50%;
|
||||
-webkit-mask-position: 50% 50%;
|
||||
}
|
||||
}`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import updateGeometry from '../update-geometry';
|
||||
|
||||
const clickRail = e => {
|
||||
const { element } = e;
|
||||
|
||||
e.event.bind(e.scrollbarYRail, 'mousedown', e => {
|
||||
const positionTop =
|
||||
e.pageY -
|
||||
window.pageYOffset -
|
||||
e.scrollbarYRail.getBoundingClientRect().top;
|
||||
const direction = positionTop > e.scrollbarYTop ? 1 : -1;
|
||||
|
||||
e.element.scrollTop += direction * e.containerHeight;
|
||||
updateGeometry(i);
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
e.event.bind(e.scrollbarY, 'mousedown', e => e.stopPropagation());
|
||||
|
||||
e.event.bind(e.scrollbarX, 'mousedown', e => e.stopPropagation());
|
||||
e.event.bind(e.scrollbarXRail, 'mousedown', e => {
|
||||
const left =
|
||||
e.pageX -
|
||||
window.pageXOffset -
|
||||
e.scrollbarXRail.getBoundingClientRect().left;
|
||||
const direction = left > e.scrollbarXLeft ? 1 : -1;
|
||||
|
||||
e.element.scrollLeft += direction * e.containerWidth;
|
||||
updateGeometry(i);
|
||||
|
||||
e.stopPropagation();
|
||||
});
|
||||
};
|
||||
export default clickRail;
|
||||
@@ -0,0 +1,104 @@
|
||||
import updateGeometry from '../update-geometry';
|
||||
import { toInt } from '../lib/util';
|
||||
import * as DOM from '../lib/dom';
|
||||
import * as CSS from '../lib/css';
|
||||
import cls, {
|
||||
addScrollingClass,
|
||||
removeScrollingClass,
|
||||
} from '../lib/class-names';
|
||||
|
||||
export default function (i) {
|
||||
bindMouseScrollHandler(i, [
|
||||
'containerWidth',
|
||||
'contentWidth',
|
||||
'pageX',
|
||||
'railXWidth',
|
||||
'scrollbarX',
|
||||
'scrollbarXWidth',
|
||||
'scrollLeft',
|
||||
'x',
|
||||
'scrollbarXRail',
|
||||
]);
|
||||
bindMouseScrollHandler(i, [
|
||||
'containerHeight',
|
||||
'contentHeight',
|
||||
'pageY',
|
||||
'railYHeight',
|
||||
'scrollbarY',
|
||||
'scrollbarYHeight',
|
||||
'scrollTop',
|
||||
'y',
|
||||
'scrollbarYRail',
|
||||
]);
|
||||
}
|
||||
|
||||
function bindMouseScrollHandler(
|
||||
i,
|
||||
[
|
||||
containerHeight,
|
||||
contentHeight,
|
||||
pageY,
|
||||
railYHeight,
|
||||
scrollbarY,
|
||||
scrollbarYHeight,
|
||||
scrollTop,
|
||||
y,
|
||||
scrollbarYRail,
|
||||
],
|
||||
) {
|
||||
const { element } = i;
|
||||
|
||||
let startingScrollTop = null;
|
||||
let startingMousePageY = null;
|
||||
let scrollBy = null;
|
||||
|
||||
function mouseMoveHandler(e) {
|
||||
if (e.touches && e.touches[0]) {
|
||||
e[pageY] = e.touches[0].pageY;
|
||||
}
|
||||
element[scrollTop] =
|
||||
startingScrollTop + scrollBy * (e[pageY] - startingMousePageY);
|
||||
addScrollingClass(i, y);
|
||||
updateGeometry(i);
|
||||
|
||||
e.stopPropagation();
|
||||
if (e.type.startsWith('touch') && e.changedTouches.length > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function mouseUpHandler() {
|
||||
removeScrollingClass(i, y);
|
||||
i[scrollbarYRail].classList.remove(cls.state.clicking);
|
||||
i.event.unbind(i.ownerDocument, 'mousemove', mouseMoveHandler);
|
||||
}
|
||||
|
||||
function bindMoves(e, touchMode) {
|
||||
startingScrollTop = element[scrollTop];
|
||||
if (touchMode && e.touches) {
|
||||
e[pageY] = e.touches[0].pageY;
|
||||
}
|
||||
startingMousePageY = e[pageY];
|
||||
scrollBy =
|
||||
(i[contentHeight] - i[containerHeight]) /
|
||||
(i[railYHeight] - i[scrollbarYHeight]);
|
||||
if (!touchMode) {
|
||||
i.event.bind(i.ownerDocument, 'mousemove', mouseMoveHandler);
|
||||
i.event.once(i.ownerDocument, 'mouseup', mouseUpHandler);
|
||||
e.preventDefault();
|
||||
} else {
|
||||
i.event.bind(i.ownerDocument, 'touchmove', mouseMoveHandler);
|
||||
}
|
||||
|
||||
i[scrollbarYRail].classList.add(cls.state.clicking);
|
||||
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
i.event.bind(i[scrollbarY], 'mousedown', e => {
|
||||
bindMoves(e);
|
||||
});
|
||||
i.event.bind(i[scrollbarY], 'touchstart', e => {
|
||||
bindMoves(e, true);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import updateGeometry from '../update-geometry';
|
||||
import { isEditable } from '../lib/util';
|
||||
import * as DOM from '../lib/dom';
|
||||
|
||||
export default function (i) {
|
||||
const { element } = i;
|
||||
|
||||
function preventDefault(x, y) {
|
||||
const scrollTop = Math.floor(element.scrollTop);
|
||||
if (x === 0) {
|
||||
if (!i.scrollbarYActive) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(scrollTop === 0 && y > 0) ||
|
||||
(scrollTop >= i.contentHeight - i.containerHeight && y < 0)
|
||||
) {
|
||||
return !i.settings.wheelPropagation;
|
||||
}
|
||||
}
|
||||
|
||||
const { scrollLeft } = element;
|
||||
if (y === 0) {
|
||||
if (!i.scrollbarXActive) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(scrollLeft === 0 && x < 0) ||
|
||||
(scrollLeft >= i.contentWidth - i.containerWidth && x > 0)
|
||||
) {
|
||||
return !i.settings.wheelPropagation;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
i.event.bind(i.ownerDocument, 'keydown', e => {
|
||||
if (
|
||||
(e.isDefaultPrevented && e.isDefaultPrevented()) ||
|
||||
e.defaultPrevented
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!DOM.matches(element, ':hover') &&
|
||||
!(
|
||||
DOM.matches(i.scrollbarX, ':focus') ||
|
||||
DOM.matches(i.scrollbarY, ':focus')
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let activeElement = document.activeElement
|
||||
? document.activeElement
|
||||
: i.ownerDocument.activeElement;
|
||||
if (activeElement) {
|
||||
if (activeElement.tagName === 'IFRAME') {
|
||||
activeElement = activeElement.contentDocument.activeElement;
|
||||
} else {
|
||||
// go deeper if element is a webcomponent
|
||||
while (activeElement.shadowRoot) {
|
||||
activeElement = activeElement.shadowRoot.activeElement;
|
||||
}
|
||||
}
|
||||
if (isEditable(activeElement)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let deltaX = 0;
|
||||
let deltaY = 0;
|
||||
|
||||
switch (e.which) {
|
||||
case 37: // left
|
||||
if (e.metaKey) {
|
||||
deltaX = -i.contentWidth;
|
||||
} else if (e.altKey) {
|
||||
deltaX = -i.containerWidth;
|
||||
} else {
|
||||
deltaX = -30;
|
||||
}
|
||||
break;
|
||||
case 38: // up
|
||||
if (e.metaKey) {
|
||||
deltaY = i.contentHeight;
|
||||
} else if (e.altKey) {
|
||||
deltaY = i.containerHeight;
|
||||
} else {
|
||||
deltaY = 30;
|
||||
}
|
||||
break;
|
||||
case 39: // right
|
||||
if (e.metaKey) {
|
||||
deltaX = i.contentWidth;
|
||||
} else if (e.altKey) {
|
||||
deltaX = i.containerWidth;
|
||||
} else {
|
||||
deltaX = 30;
|
||||
}
|
||||
break;
|
||||
case 40: // down
|
||||
if (e.metaKey) {
|
||||
deltaY = -i.contentHeight;
|
||||
} else if (e.altKey) {
|
||||
deltaY = -i.containerHeight;
|
||||
} else {
|
||||
deltaY = -30;
|
||||
}
|
||||
break;
|
||||
case 32: // space bar
|
||||
if (e.shiftKey) {
|
||||
deltaY = i.containerHeight;
|
||||
} else {
|
||||
deltaY = -i.containerHeight;
|
||||
}
|
||||
break;
|
||||
case 33: // page up
|
||||
deltaY = i.containerHeight;
|
||||
break;
|
||||
case 34: // page down
|
||||
deltaY = -i.containerHeight;
|
||||
break;
|
||||
case 36: // home
|
||||
deltaY = i.contentHeight;
|
||||
break;
|
||||
case 35: // end
|
||||
deltaY = -i.contentHeight;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (i.settings.suppressScrollX && deltaX !== 0) {
|
||||
return;
|
||||
}
|
||||
if (i.settings.suppressScrollY && deltaY !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.scrollTop -= deltaY;
|
||||
element.scrollLeft += deltaX;
|
||||
updateGeometry(i);
|
||||
|
||||
if (preventDefault(deltaX, deltaY)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import updateGeometry from '../update-geometry';
|
||||
import { env } from '../lib/util';
|
||||
import * as CSS from '../lib/css';
|
||||
import cls from '../lib/class-names';
|
||||
|
||||
export default function (i) {
|
||||
const { element } = i;
|
||||
|
||||
const shouldPrevent = false;
|
||||
|
||||
function shouldPreventDefault(deltaX, deltaY) {
|
||||
const roundedScrollTop = Math.floor(element.scrollTop);
|
||||
const isTop = element.scrollTop === 0;
|
||||
const isBottom =
|
||||
roundedScrollTop + element.offsetHeight === element.scrollHeight;
|
||||
const isLeft = element.scrollLeft === 0;
|
||||
const isRight =
|
||||
element.scrollLeft + element.offsetWidth === element.scrollWidth;
|
||||
|
||||
let hitsBound;
|
||||
|
||||
// pick axis with primary direction
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||
hitsBound = isTop || isBottom;
|
||||
} else {
|
||||
hitsBound = isLeft || isRight;
|
||||
}
|
||||
|
||||
return hitsBound ? !i.settings.wheelPropagation : true;
|
||||
}
|
||||
|
||||
function getDeltaFromEvent(e) {
|
||||
let { deltaX } = e;
|
||||
let deltaY = -1 * e.deltaY;
|
||||
|
||||
if (typeof deltaX === 'undefined' || typeof deltaY === 'undefined') {
|
||||
// OS X Safari
|
||||
deltaX = (-1 * e.wheelDeltaX) / 6;
|
||||
deltaY = e.wheelDeltaY / 6;
|
||||
}
|
||||
|
||||
if (e.deltaMode && e.deltaMode === 1) {
|
||||
// Firefox in deltaMode 1: Line scrolling
|
||||
deltaX *= 10;
|
||||
deltaY *= 10;
|
||||
}
|
||||
|
||||
if (deltaX !== deltaX && deltaY !== deltaY /* NaN checks */) {
|
||||
// IE in some mouse drivers
|
||||
deltaX = 0;
|
||||
deltaY = e.wheelDelta;
|
||||
}
|
||||
|
||||
if (e.shiftKey) {
|
||||
// reverse axis with shift key
|
||||
return [-deltaY, -deltaX];
|
||||
}
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||
deltaX = 0;
|
||||
}
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
deltaY = 0;
|
||||
}
|
||||
return [deltaX, deltaY];
|
||||
}
|
||||
|
||||
function shouldBeConsumedByChild(target, deltaX, deltaY) {
|
||||
// FIXME: this is a workaround for <select> issue in FF and IE #571
|
||||
if (!env.isWebKit && element.querySelector('select:focus')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!element.contains(target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let cursor = target;
|
||||
|
||||
while (cursor && cursor !== element) {
|
||||
if (cursor.classList.contains(cls.element.consuming)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const style = CSS.get(cursor);
|
||||
|
||||
// if deltaY && vertical scrollable
|
||||
if (deltaY && style.overflowY.match(/(scroll|auto)/)) {
|
||||
const maxScrollTop = cursor.scrollHeight - cursor.clientHeight;
|
||||
if (maxScrollTop > 0) {
|
||||
if (
|
||||
(cursor.scrollTop > 0 && deltaY < 0) ||
|
||||
(cursor.scrollTop < maxScrollTop && deltaY > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if deltaX && horizontal scrollable
|
||||
if (deltaX && style.overflowX.match(/(scroll|auto)/)) {
|
||||
const maxScrollLeft = cursor.scrollWidth - cursor.clientWidth;
|
||||
if (maxScrollLeft > 0) {
|
||||
if (
|
||||
(cursor.scrollLeft > 0 && deltaX < 0) ||
|
||||
(cursor.scrollLeft < maxScrollLeft && deltaX > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor = cursor.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function mousewheelHandler(e) {
|
||||
const [deltaX, deltaY] = getDeltaFromEvent(e);
|
||||
|
||||
// FIXME: mousewheel 滚动的时候有问题
|
||||
if (shouldBeConsumedByChild(e.target, deltaX, deltaY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldPrevent = false;
|
||||
if (!i.settings.useBothWheelAxes) {
|
||||
// deltaX will only be used for horizontal scrolling and deltaY will
|
||||
// only be used for vertical scrolling - this is the default
|
||||
element.scrollTop -= deltaY * i.settings.wheelSpeed;
|
||||
element.scrollLeft += deltaX * i.settings.wheelSpeed;
|
||||
} else if (i.scrollbarYActive && !i.scrollbarXActive) {
|
||||
// only vertical scrollbar is active and useBothWheelAxes option is
|
||||
// active, so let's scroll vertical bar using both mouse wheel axes
|
||||
if (deltaY) {
|
||||
element.scrollTop -= deltaY * i.settings.wheelSpeed;
|
||||
} else {
|
||||
element.scrollTop += deltaX * i.settings.wheelSpeed;
|
||||
}
|
||||
shouldPrevent = true;
|
||||
} else if (i.scrollbarXActive && !i.scrollbarYActive) {
|
||||
// useBothWheelAxes and only horizontal bar is active, so use both
|
||||
// wheel axes for horizontal bar
|
||||
if (deltaX) {
|
||||
element.scrollLeft += deltaX * i.settings.wheelSpeed;
|
||||
} else {
|
||||
element.scrollLeft -= deltaY * i.settings.wheelSpeed;
|
||||
}
|
||||
shouldPrevent = true;
|
||||
}
|
||||
|
||||
updateGeometry(i);
|
||||
|
||||
shouldPrevent = shouldPrevent || shouldPreventDefault(deltaX, deltaY);
|
||||
if (shouldPrevent && !e.ctrlKey) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window.onwheel !== 'undefined') {
|
||||
i.event.bind(element, 'wheel', mousewheelHandler);
|
||||
} else if (typeof window.onmousewheel !== 'undefined') {
|
||||
i.event.bind(element, 'mousewheel', mousewheelHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import updateGeometry from '../update-geometry';
|
||||
import { env } from '../lib/util';
|
||||
import * as CSS from '../lib/css';
|
||||
import cls from '../lib/class-names';
|
||||
|
||||
export default function (i) {
|
||||
if (!env.supportsTouch && !env.supportsIePointer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { element } = i;
|
||||
|
||||
function shouldPrevent(deltaX, deltaY) {
|
||||
const scrollTop = Math.floor(element.scrollTop);
|
||||
const { scrollLeft } = element;
|
||||
const magnitudeX = Math.abs(deltaX);
|
||||
const magnitudeY = Math.abs(deltaY);
|
||||
|
||||
if (magnitudeY > magnitudeX) {
|
||||
// user is perhaps trying to swipe up/down the page
|
||||
|
||||
if (
|
||||
(deltaY < 0 && scrollTop === i.contentHeight - i.containerHeight) ||
|
||||
(deltaY > 0 && scrollTop === 0)
|
||||
) {
|
||||
// set prevent for mobile Chrome refresh
|
||||
return window.scrollY === 0 && deltaY > 0 && env.isChrome;
|
||||
}
|
||||
} else if (magnitudeX > magnitudeY) {
|
||||
// user is perhaps trying to swipe left/right across the page
|
||||
|
||||
if (
|
||||
(deltaX < 0 && scrollLeft === i.contentWidth - i.containerWidth) ||
|
||||
(deltaX > 0 && scrollLeft === 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyTouchMove(differenceX, differenceY) {
|
||||
element.scrollTop -= differenceY;
|
||||
element.scrollLeft -= differenceX;
|
||||
|
||||
updateGeometry(i);
|
||||
}
|
||||
|
||||
let startOffset = {};
|
||||
let startTime = 0;
|
||||
const speed = {};
|
||||
let easingLoop = null;
|
||||
|
||||
function getTouch(e) {
|
||||
if (e.targetTouches) {
|
||||
return e.targetTouches[0];
|
||||
} else {
|
||||
// Maybe IE pointer
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldHandle(e) {
|
||||
if (e.pointerType && e.pointerType === 'pen' && e.buttons === 0) {
|
||||
return false;
|
||||
}
|
||||
if (e.targetTouches && e.targetTouches.length === 1) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
e.pointerType &&
|
||||
e.pointerType !== 'mouse' &&
|
||||
e.pointerType !== e.MSPOINTER_TYPE_MOUSE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function touchStart(e) {
|
||||
if (!shouldHandle(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = getTouch(e);
|
||||
|
||||
startOffset.pageX = touch.pageX;
|
||||
startOffset.pageY = touch.pageY;
|
||||
|
||||
startTime = new Date().getTime();
|
||||
|
||||
if (easingLoop !== null) {
|
||||
clearInterval(easingLoop);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldBeConsumedByChild(target, deltaX, deltaY) {
|
||||
if (!element.contains(target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let cursor = target;
|
||||
|
||||
while (cursor && cursor !== element) {
|
||||
if (cursor.classList.contains(cls.element.consuming)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const style = CSS.get(cursor);
|
||||
|
||||
// if deltaY && vertical scrollable
|
||||
if (deltaY && style.overflowY.match(/(scroll|auto)/)) {
|
||||
const maxScrollTop = cursor.scrollHeight - cursor.clientHeight;
|
||||
if (maxScrollTop > 0) {
|
||||
if (
|
||||
(cursor.scrollTop > 0 && deltaY < 0) ||
|
||||
(cursor.scrollTop < maxScrollTop && deltaY > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if deltaX && horizontal scrollable
|
||||
if (deltaX && style.overflowX.match(/(scroll|auto)/)) {
|
||||
const maxScrollLeft = cursor.scrollWidth - cursor.clientWidth;
|
||||
if (maxScrollLeft > 0) {
|
||||
if (
|
||||
(cursor.scrollLeft > 0 && deltaX < 0) ||
|
||||
(cursor.scrollLeft < maxScrollLeft && deltaX > 0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor = cursor.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function touchMove(e) {
|
||||
if (shouldHandle(e)) {
|
||||
const touch = getTouch(e);
|
||||
|
||||
const currentOffset = { pageX: touch.pageX, pageY: touch.pageY };
|
||||
|
||||
const differenceX = currentOffset.pageX - startOffset.pageX;
|
||||
const differenceY = currentOffset.pageY - startOffset.pageY;
|
||||
|
||||
if (shouldBeConsumedByChild(e.target, differenceX, differenceY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyTouchMove(differenceX, differenceY);
|
||||
startOffset = currentOffset;
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
|
||||
const timeGap = currentTime - startTime;
|
||||
if (timeGap > 0) {
|
||||
speed.x = differenceX / timeGap;
|
||||
speed.y = differenceY / timeGap;
|
||||
startTime = currentTime;
|
||||
}
|
||||
|
||||
if (shouldPrevent(differenceX, differenceY)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
function touchEnd() {
|
||||
if (i.settings.swipeEasing) {
|
||||
clearInterval(easingLoop);
|
||||
easingLoop = setInterval(function () {
|
||||
if (i.isInitialized) {
|
||||
clearInterval(easingLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!speed.x && !speed.y) {
|
||||
clearInterval(easingLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(speed.x) < 0.01 && Math.abs(speed.y) < 0.01) {
|
||||
clearInterval(easingLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!i.element) {
|
||||
clearInterval(easingLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
applyTouchMove(speed.x * 30, speed.y * 30);
|
||||
|
||||
speed.x *= 0.8;
|
||||
speed.y *= 0.8;
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
if (env.supportsTouch) {
|
||||
i.event.bind(element, 'touchstart', touchStart);
|
||||
i.event.bind(element, 'touchmove', touchMove);
|
||||
i.event.bind(element, 'touchend', touchEnd);
|
||||
} else if (env.supportsIePointer) {
|
||||
if (window.PointerEvent) {
|
||||
i.event.bind(element, 'pointerdown', touchStart);
|
||||
i.event.bind(element, 'pointermove', touchMove);
|
||||
i.event.bind(element, 'pointerup', touchEnd);
|
||||
} else if (window.MSPointerEvent) {
|
||||
i.event.bind(element, 'MSPointerDown', touchStart);
|
||||
i.event.bind(element, 'MSPointerMove', touchMove);
|
||||
i.event.bind(element, 'MSPointerUp', touchEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import updateGeometry from './update-geometry';
|
||||
import processScrollDiff from './process-scroll-diff';
|
||||
import { toInt, outerWidth } from './lib/util';
|
||||
import EventManager from './lib/event-manager';
|
||||
import * as DOM from './lib/dom';
|
||||
import * as CSS from './lib/css';
|
||||
import cls from './lib/class-names';
|
||||
import touch from './handlers/touch';
|
||||
import wheel from './handlers/mouse-wheel';
|
||||
import keyboard from './handlers/keyboard';
|
||||
import dragThumb from './handlers/drag-thumb';
|
||||
import clickRail from './handlers/click-rail';
|
||||
|
||||
const defaultSettings = () => ({
|
||||
handlers: ['click-rail', 'drag-thumb', 'keyboard', 'wheel', 'touch'],
|
||||
maxScrollbarLength: null,
|
||||
minScrollbarLength: null,
|
||||
scrollingThreshold: 1000,
|
||||
scrollXMarginOffset: 0,
|
||||
scrollYMarginOffset: 0,
|
||||
suppressScrollX: false,
|
||||
suppressScrollY: false,
|
||||
swipeEasing: true,
|
||||
useBothWheelAxes: false,
|
||||
wheelPropagation: true,
|
||||
wheelSpeed: 1,
|
||||
});
|
||||
|
||||
const handlers = {
|
||||
'click-rail': clickRail,
|
||||
'drag-thumb': dragThumb,
|
||||
keyboard,
|
||||
wheel,
|
||||
touch,
|
||||
};
|
||||
|
||||
export default class PerfectScrollbar {
|
||||
constructor(element, userSettings = {}) {
|
||||
if (typeof element === 'string') {
|
||||
element = document.querySelector(element);
|
||||
}
|
||||
|
||||
if (!element || !element.nodeName) {
|
||||
throw new Error('no element is specified to initialize PerfectScrollbar');
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
|
||||
element.classList.add(cls.main);
|
||||
|
||||
this.settings = defaultSettings();
|
||||
for (const key in userSettings) {
|
||||
this.settings[key] = userSettings[key];
|
||||
}
|
||||
|
||||
this.containerWidth = null;
|
||||
this.containerHeight = null;
|
||||
this.contentWidth = null;
|
||||
this.contentHeight = null;
|
||||
|
||||
const focus = () => element.classList.add(cls.state.focus);
|
||||
const blur = () => element.classList.remove(cls.state.focus);
|
||||
|
||||
this.isRtl = CSS.get(element).direction === 'rtl';
|
||||
if (this.isRtl === true) {
|
||||
element.classList.add(cls.rtl);
|
||||
}
|
||||
this.isNegativeScroll = (() => {
|
||||
const originalScrollLeft = element.scrollLeft;
|
||||
let result = null;
|
||||
element.scrollLeft = -1;
|
||||
result = element.scrollLeft < 0;
|
||||
element.scrollLeft = originalScrollLeft;
|
||||
return result;
|
||||
})();
|
||||
this.negativeScrollAdjustment = this.isNegativeScroll
|
||||
? element.scrollWidth - element.clientWidth
|
||||
: 0;
|
||||
this.event = new EventManager();
|
||||
this.ownerDocument = element.ownerDocument || document;
|
||||
|
||||
this.scrollbarXRail = DOM.div(cls.element.rail('x'));
|
||||
element.appendChild(this.scrollbarXRail);
|
||||
this.scrollbarX = DOM.div(cls.element.thumb('x'));
|
||||
this.scrollbarXRail.appendChild(this.scrollbarX);
|
||||
this.scrollbarX.setAttribute('tabindex', 0);
|
||||
this.event.bind(this.scrollbarX, 'focus', focus);
|
||||
this.event.bind(this.scrollbarX, 'blur', blur);
|
||||
this.scrollbarXActive = null;
|
||||
this.scrollbarXWidth = null;
|
||||
this.scrollbarXLeft = null;
|
||||
const railXStyle = CSS.get(this.scrollbarXRail);
|
||||
this.scrollbarXBottom = parseInt(railXStyle.bottom, 10);
|
||||
if (isNaN(this.scrollbarXBottom)) {
|
||||
this.isScrollbarXUsingBottom = false;
|
||||
this.scrollbarXTop = toInt(railXStyle.top);
|
||||
} else {
|
||||
this.isScrollbarXUsingBottom = true;
|
||||
}
|
||||
this.railBorderXWidth =
|
||||
toInt(railXStyle.borderLeftWidth) + toInt(railXStyle.borderRightWidth);
|
||||
// Set rail to display:block to calculate margins
|
||||
CSS.set(this.scrollbarXRail, { display: 'block' });
|
||||
this.railXMarginWidth =
|
||||
toInt(railXStyle.marginLeft) + toInt(railXStyle.marginRight);
|
||||
CSS.set(this.scrollbarXRail, { display: '' });
|
||||
this.railXWidth = null;
|
||||
this.railXRatio = null;
|
||||
|
||||
this.scrollbarYRail = DOM.div(cls.element.rail('y'));
|
||||
element.appendChild(this.scrollbarYRail);
|
||||
this.scrollbarY = DOM.div(cls.element.thumb('y'));
|
||||
this.scrollbarYRail.appendChild(this.scrollbarY);
|
||||
this.scrollbarY.setAttribute('tabindex', 0);
|
||||
this.event.bind(this.scrollbarY, 'focus', focus);
|
||||
this.event.bind(this.scrollbarY, 'blur', blur);
|
||||
this.scrollbarYActive = null;
|
||||
this.scrollbarYHeight = null;
|
||||
this.scrollbarYTop = null;
|
||||
const railYStyle = CSS.get(this.scrollbarYRail);
|
||||
this.scrollbarYRight = parseInt(railYStyle.right, 10);
|
||||
if (isNaN(this.scrollbarYRight)) {
|
||||
this.isScrollbarYUsingRight = false;
|
||||
this.scrollbarYLeft = toInt(railYStyle.left);
|
||||
} else {
|
||||
this.isScrollbarYUsingRight = true;
|
||||
}
|
||||
this.scrollbarYOuterWidth = this.isRtl ? outerWidth(this.scrollbarY) : null;
|
||||
this.railBorderYWidth =
|
||||
toInt(railYStyle.borderTopWidth) + toInt(railYStyle.borderBottomWidth);
|
||||
CSS.set(this.scrollbarYRail, { display: 'block' });
|
||||
this.railYMarginHeight =
|
||||
toInt(railYStyle.marginTop) + toInt(railYStyle.marginBottom);
|
||||
CSS.set(this.scrollbarYRail, { display: '' });
|
||||
this.railYHeight = null;
|
||||
this.railYRatio = null;
|
||||
|
||||
this.reach = {
|
||||
x:
|
||||
element.scrollLeft <= 0
|
||||
? 'start'
|
||||
: element.scrollLeft >= this.contentWidth - this.containerWidth
|
||||
? 'end'
|
||||
: null,
|
||||
y:
|
||||
element.scrollTop <= 0
|
||||
? 'start'
|
||||
: element.scrollTop >= this.contentHeight - this.containerHeight
|
||||
? 'end'
|
||||
: null,
|
||||
};
|
||||
|
||||
this.isAlive = true;
|
||||
|
||||
this.settings.handlers.forEach(handlerName => handlers[handlerName](this));
|
||||
|
||||
this.lastScrollTop = Math.floor(element.scrollTop); // for onScroll only
|
||||
this.lastScrollLeft = element.scrollLeft; // for onScroll only
|
||||
this.event.bind(this.element, 'scroll', e => this.onScroll(e));
|
||||
updateGeometry(this);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.isAlive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Recalcuate negative scrollLeft adjustment
|
||||
this.negativeScrollAdjustment = this.isNegativeScroll
|
||||
? this.element.scrollWidth - this.element.clientWidth
|
||||
: 0;
|
||||
|
||||
// Recalculate rail margins
|
||||
CSS.set(this.scrollbarXRail, { display: 'block' });
|
||||
CSS.set(this.scrollbarYRail, { display: 'block' });
|
||||
this.railXMarginWidth =
|
||||
toInt(CSS.get(this.scrollbarXRail).marginLeft) +
|
||||
toInt(CSS.get(this.scrollbarXRail).marginRight);
|
||||
this.railYMarginHeight =
|
||||
toInt(CSS.get(this.scrollbarYRail).marginTop) +
|
||||
toInt(CSS.get(this.scrollbarYRail).marginBottom);
|
||||
|
||||
// Hide scrollbars not to affect scrollWidth and scrollHeight
|
||||
CSS.set(this.scrollbarXRail, { display: 'none' });
|
||||
CSS.set(this.scrollbarYRail, { display: 'none' });
|
||||
|
||||
updateGeometry(this);
|
||||
|
||||
processScrollDiff(this, 'top', 0, false, true);
|
||||
processScrollDiff(this, 'left', 0, false, true);
|
||||
|
||||
CSS.set(this.scrollbarXRail, { display: '' });
|
||||
CSS.set(this.scrollbarYRail, { display: '' });
|
||||
}
|
||||
|
||||
onScroll(e) {
|
||||
if (!this.isAlive) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateGeometry(this);
|
||||
processScrollDiff(this, 'top', this.element.scrollTop - this.lastScrollTop);
|
||||
processScrollDiff(
|
||||
this,
|
||||
'left',
|
||||
this.element.scrollLeft - this.lastScrollLeft,
|
||||
);
|
||||
|
||||
this.lastScrollTop = Math.floor(this.element.scrollTop);
|
||||
this.lastScrollLeft = this.element.scrollLeft;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.isAlive) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.event.unbindAll();
|
||||
DOM.remove(this.scrollbarX);
|
||||
DOM.remove(this.scrollbarY);
|
||||
DOM.remove(this.scrollbarXRail);
|
||||
DOM.remove(this.scrollbarYRail);
|
||||
this.removePsClasses();
|
||||
|
||||
// unset elements
|
||||
this.element = null;
|
||||
this.scrollbarX = null;
|
||||
this.scrollbarY = null;
|
||||
this.scrollbarXRail = null;
|
||||
this.scrollbarYRail = null;
|
||||
|
||||
this.isAlive = false;
|
||||
}
|
||||
|
||||
removePsClasses() {
|
||||
this.element.className = this.element.className
|
||||
.split(' ')
|
||||
.filter(name => !name.match(/^ps([-_].+|)$/))
|
||||
.join(' ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
const cls = {
|
||||
main: 'ide-ps',
|
||||
rtl: 'ide-ps__rtl',
|
||||
element: {
|
||||
thumb: x => `ide-ps__thumb-${x}`,
|
||||
rail: x => `ide-ps__rail-${x}`,
|
||||
consuming: 'ide-ps__child--consume',
|
||||
},
|
||||
state: {
|
||||
focus: 'ide-ps--focus',
|
||||
clicking: 'ide-ps--clicking',
|
||||
active: x => `ide-ps--active-${x}`,
|
||||
scrolling: x => `ide-ps--scrolling-${x}`,
|
||||
},
|
||||
};
|
||||
|
||||
export default cls;
|
||||
|
||||
/*
|
||||
* Helper methods
|
||||
*/
|
||||
const scrollingClassTimeout = { x: null, y: null };
|
||||
|
||||
export function addScrollingClass(i, x) {
|
||||
const classList = i.element.classList;
|
||||
const className = cls.state.scrolling(x);
|
||||
|
||||
if (classList.contains(className)) {
|
||||
clearTimeout(scrollingClassTimeout[x]);
|
||||
} else {
|
||||
classList.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeScrollingClass(i, x) {
|
||||
scrollingClassTimeout[x] = setTimeout(
|
||||
() => i.isAlive && i.element.classList.remove(cls.state.scrolling(x)),
|
||||
i.settings.scrollingThreshold
|
||||
);
|
||||
}
|
||||
|
||||
export function setScrollingClassInstantly(i, x) {
|
||||
addScrollingClass(i, x);
|
||||
removeScrollingClass(i, x);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export function get(element) {
|
||||
return getComputedStyle(element);
|
||||
}
|
||||
|
||||
export function set(element, obj) {
|
||||
for (const key in obj) {
|
||||
let val = obj[key];
|
||||
if (typeof val === 'number') {
|
||||
val = `${val}px`;
|
||||
}
|
||||
element.style[key] = val;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
export function div(className) {
|
||||
const div = document.createElement('div');
|
||||
div.className = className;
|
||||
return div;
|
||||
}
|
||||
|
||||
const elMatches =
|
||||
typeof Element !== 'undefined' &&
|
||||
(Element.prototype.matches ||
|
||||
Element.prototype.webkitMatchesSelector ||
|
||||
Element.prototype.mozMatchesSelector ||
|
||||
Element.prototype.msMatchesSelector);
|
||||
|
||||
export function matches(element, query) {
|
||||
if (!elMatches) {
|
||||
throw new Error('No element matching method supported');
|
||||
}
|
||||
|
||||
return elMatches.call(element, query);
|
||||
}
|
||||
|
||||
export function remove(element) {
|
||||
if (element.remove) {
|
||||
element.remove();
|
||||
} else {
|
||||
if (element.parentNode) {
|
||||
element.parentNode.removeChild(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function queryChildren(element, selector) {
|
||||
return Array.prototype.filter.call(element.children, child =>
|
||||
matches(child, selector)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
class EventElement {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.handlers = {};
|
||||
}
|
||||
|
||||
bind(eventName, handler) {
|
||||
if (typeof this.handlers[eventName] === 'undefined') {
|
||||
this.handlers[eventName] = [];
|
||||
}
|
||||
this.handlers[eventName].push(handler);
|
||||
this.element.addEventListener(eventName, handler, false);
|
||||
}
|
||||
|
||||
unbind(eventName, target) {
|
||||
this.handlers[eventName] = this.handlers[eventName].filter(handler => {
|
||||
if (target && handler !== target) {
|
||||
return true;
|
||||
}
|
||||
this.element.removeEventListener(eventName, handler, false);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
unbindAll() {
|
||||
for (const name in this.handlers) {
|
||||
this.unbind(name);
|
||||
}
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return Object.keys(this.handlers).every(
|
||||
key => this.handlers[key].length === 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class EventManager {
|
||||
constructor() {
|
||||
this.eventElements = [];
|
||||
}
|
||||
|
||||
eventElement(element) {
|
||||
let ee = this.eventElements.filter(ee => ee.element === element)[0];
|
||||
if (!ee) {
|
||||
ee = new EventElement(element);
|
||||
this.eventElements.push(ee);
|
||||
}
|
||||
return ee;
|
||||
}
|
||||
|
||||
bind(element, eventName, handler) {
|
||||
this.eventElement(element).bind(eventName, handler);
|
||||
}
|
||||
|
||||
unbind(element, eventName, handler) {
|
||||
const ee = this.eventElement(element);
|
||||
ee.unbind(eventName, handler);
|
||||
|
||||
if (ee.isEmpty) {
|
||||
// remove
|
||||
this.eventElements.splice(this.eventElements.indexOf(ee), 1);
|
||||
}
|
||||
}
|
||||
|
||||
unbindAll() {
|
||||
this.eventElements.forEach(e => e.unbindAll());
|
||||
this.eventElements = [];
|
||||
}
|
||||
|
||||
once(element, eventName, handler) {
|
||||
const ee = this.eventElement(element);
|
||||
const onceHandler = evt => {
|
||||
ee.unbind(eventName, onceHandler);
|
||||
handler(evt);
|
||||
};
|
||||
ee.bind(eventName, onceHandler);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as CSS from './css';
|
||||
import * as DOM from './dom';
|
||||
|
||||
export function toInt(x) {
|
||||
return parseInt(x, 10) || 0;
|
||||
}
|
||||
|
||||
export function isEditable(el) {
|
||||
return (
|
||||
DOM.matches(el, 'input,[contenteditable]') ||
|
||||
DOM.matches(el, 'select,[contenteditable]') ||
|
||||
DOM.matches(el, 'textarea,[contenteditable]') ||
|
||||
DOM.matches(el, 'button,[contenteditable]')
|
||||
);
|
||||
}
|
||||
|
||||
export function outerWidth(element) {
|
||||
const styles = CSS.get(element);
|
||||
return (
|
||||
toInt(styles.width) +
|
||||
toInt(styles.paddingLeft) +
|
||||
toInt(styles.paddingRight) +
|
||||
toInt(styles.borderLeftWidth) +
|
||||
toInt(styles.borderRightWidth)
|
||||
);
|
||||
}
|
||||
|
||||
export const env = {
|
||||
isWebKit:
|
||||
typeof document !== 'undefined' &&
|
||||
'WebkitAppearance' in document.documentElement.style,
|
||||
supportsTouch:
|
||||
typeof window !== 'undefined' &&
|
||||
('ontouchstart' in window ||
|
||||
('maxTouchPoints' in window.navigator &&
|
||||
window.navigator.maxTouchPoints > 0) ||
|
||||
(window.DocumentTouch && document instanceof window.DocumentTouch)),
|
||||
supportsIePointer:
|
||||
typeof navigator !== 'undefined' && navigator.msMaxTouchPoints,
|
||||
isChrome:
|
||||
typeof navigator !== 'undefined' &&
|
||||
/Chrome/i.test(navigator && navigator.userAgent),
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { setScrollingClassInstantly } from './lib/class-names';
|
||||
|
||||
function createEvent(name) {
|
||||
if (typeof window.CustomEvent === 'function') {
|
||||
return new CustomEvent(name);
|
||||
} else {
|
||||
const evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent(name, false, false, undefined);
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
|
||||
export default function (
|
||||
i,
|
||||
axis,
|
||||
diff,
|
||||
useScrollingClass = true,
|
||||
forceFireReachEvent = false,
|
||||
) {
|
||||
let fields;
|
||||
if (axis === 'top') {
|
||||
fields = [
|
||||
'contentHeight',
|
||||
'containerHeight',
|
||||
'scrollTop',
|
||||
'y',
|
||||
'up',
|
||||
'down',
|
||||
];
|
||||
} else if (axis === 'left') {
|
||||
fields = [
|
||||
'contentWidth',
|
||||
'containerWidth',
|
||||
'scrollLeft',
|
||||
'x',
|
||||
'left',
|
||||
'right',
|
||||
];
|
||||
} else {
|
||||
throw new Error('A proper axis should be provided');
|
||||
}
|
||||
|
||||
processScrollDiff(i, diff, fields, useScrollingClass, forceFireReachEvent);
|
||||
}
|
||||
|
||||
function processScrollDiff(
|
||||
i,
|
||||
diff,
|
||||
[contentHeight, containerHeight, scrollTop, y, up, down],
|
||||
useScrollingClass = true,
|
||||
forceFireReachEvent = false,
|
||||
) {
|
||||
const { element } = i;
|
||||
|
||||
// reset reach
|
||||
i.reach[y] = null;
|
||||
|
||||
// 1 for subpixel rounding
|
||||
if (element[scrollTop] < 1) {
|
||||
i.reach[y] = 'start';
|
||||
}
|
||||
|
||||
// 1 for subpixel rounding
|
||||
if (element[scrollTop] > i[contentHeight] - i[containerHeight] - 1) {
|
||||
i.reach[y] = 'end';
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
element.dispatchEvent(createEvent(`ide-ps-scroll-${y}`));
|
||||
|
||||
if (diff < 0) {
|
||||
element.dispatchEvent(createEvent(`ide-ps-scroll-${up}`));
|
||||
} else if (diff > 0) {
|
||||
element.dispatchEvent(createEvent(`ide-ps-scroll-${down}`));
|
||||
}
|
||||
|
||||
if (useScrollingClass) {
|
||||
setScrollingClassInstantly(i, y);
|
||||
}
|
||||
}
|
||||
|
||||
if (i.reach[y] && (diff || forceFireReachEvent)) {
|
||||
element.dispatchEvent(createEvent(`ide-ps-${y}-reach-${i.reach[y]}`));
|
||||
}
|
||||
}
|
||||
78
frontend/packages/project-ide/view/src/components/scroll-bar/types/perfect-scrollbar.d.ts
vendored
Normal file
78
frontend/packages/project-ide/view/src/components/scroll-bar/types/perfect-scrollbar.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
declare namespace PerfectScrollbar {
|
||||
export interface Options {
|
||||
handlers?: string[];
|
||||
maxScrollbarLength?: number;
|
||||
minScrollbarLength?: number;
|
||||
scrollingThreshold?: number;
|
||||
scrollXMarginOffset?: number;
|
||||
scrollYMarginOffset?: number;
|
||||
suppressScrollX?: boolean;
|
||||
suppressScrollY?: boolean;
|
||||
swipeEasing?: boolean;
|
||||
useBothWheelAxes?: boolean;
|
||||
wheelPropagation?: boolean;
|
||||
wheelSpeed?: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare class PerfectScrollbar {
|
||||
constructor(element: string | Element, options?: PerfectScrollbar.Options);
|
||||
|
||||
update(): void;
|
||||
destroy(): void;
|
||||
|
||||
containerHeight: number;
|
||||
containerWidth: number;
|
||||
contentHeight: number;
|
||||
contentWidth: number;
|
||||
element: HTMLElement;
|
||||
isAlive: boolean;
|
||||
isNegativeScroll: boolean;
|
||||
isRtl: boolean;
|
||||
isScrollbarXUsingBottom: boolean;
|
||||
isScrollbarYUsingBottom: boolean;
|
||||
lastScrollLeft: boolean;
|
||||
lastScrollTop: boolean;
|
||||
negativeScrollAdjustment: number;
|
||||
railBorderXWidth: number;
|
||||
railBorderYWidth: number;
|
||||
railXMarginWidth: number;
|
||||
railXRatio: number;
|
||||
railXWidth: number;
|
||||
railYHeight: number;
|
||||
railYMarginHeight: number;
|
||||
railYRatio: number;
|
||||
scrollbarX: HTMLElement;
|
||||
scrollbarXActive: boolean;
|
||||
scrollbarXBottom: number;
|
||||
scrollbarXLeft: number;
|
||||
scrollbarXRail: HTMLElement;
|
||||
scrollbarXWidth: number;
|
||||
scrollbarY: HTMLElement;
|
||||
scrollbarYActive: boolean;
|
||||
scrollbarYHeight: number;
|
||||
scrollbarYOuterWidth?: number;
|
||||
scrollbarYRail: HTMLElement;
|
||||
scrollbarYRight: number;
|
||||
scrollbarYTop: number;
|
||||
settings: PerfectScrollbar.Options;
|
||||
reach: { x: 'start' | 'end' | null, y: 'start' | 'end' | null };
|
||||
}
|
||||
|
||||
export default PerfectScrollbar;
|
||||
@@ -0,0 +1,170 @@
|
||||
import { toInt } from './lib/util';
|
||||
import * as DOM from './lib/dom';
|
||||
import * as CSS from './lib/css';
|
||||
import cls from './lib/class-names';
|
||||
|
||||
export default function (i, direction) {
|
||||
const { element } = i;
|
||||
const roundedScrollTop = Math.floor(element.scrollTop);
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
i.containerWidth = Math.round(rect.width);
|
||||
i.containerHeight = Math.round(rect.height);
|
||||
|
||||
i.contentWidth = element.scrollWidth;
|
||||
i.contentHeight = element.scrollHeight;
|
||||
|
||||
if (
|
||||
!element.contains(i.scrollbarXRail) &&
|
||||
(!direction || direction === 'x')
|
||||
) {
|
||||
// clean up and append
|
||||
DOM.queryChildren(element, cls.element.rail('x')).forEach(el =>
|
||||
DOM.remove(el),
|
||||
);
|
||||
element.appendChild(i.scrollbarXRail);
|
||||
}
|
||||
if (
|
||||
!element.contains(i.scrollbarYRail) &&
|
||||
(!direction || direction === 'y')
|
||||
) {
|
||||
// clean up and append
|
||||
DOM.queryChildren(element, cls.element.rail('y')).forEach(el =>
|
||||
DOM.remove(el),
|
||||
);
|
||||
element.appendChild(i.scrollbarYRail);
|
||||
}
|
||||
|
||||
if (
|
||||
!i.settings.suppressScrollX &&
|
||||
i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth
|
||||
) {
|
||||
i.scrollbarXActive = true;
|
||||
i.railXWidth = i.containerWidth - i.railXMarginWidth;
|
||||
i.railXRatio = i.containerWidth / i.railXWidth;
|
||||
i.scrollbarXWidth = getThumbSize(
|
||||
i,
|
||||
toInt((i.railXWidth * i.containerWidth) / i.contentWidth),
|
||||
);
|
||||
i.scrollbarXLeft = toInt(
|
||||
((i.negativeScrollAdjustment + element.scrollLeft) *
|
||||
(i.railXWidth - i.scrollbarXWidth)) /
|
||||
(i.contentWidth - i.containerWidth),
|
||||
);
|
||||
} else {
|
||||
i.scrollbarXActive = false;
|
||||
}
|
||||
|
||||
if (
|
||||
!i.settings.suppressScrollY &&
|
||||
i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight
|
||||
) {
|
||||
i.scrollbarYActive = true;
|
||||
i.railYHeight = i.containerHeight - i.railYMarginHeight;
|
||||
i.railYRatio = i.containerHeight / i.railYHeight;
|
||||
i.scrollbarYHeight = getThumbSize(
|
||||
i,
|
||||
toInt((i.railYHeight * i.containerHeight) / i.contentHeight),
|
||||
);
|
||||
i.scrollbarYTop = toInt(
|
||||
(roundedScrollTop * (i.railYHeight - i.scrollbarYHeight)) /
|
||||
(i.contentHeight - i.containerHeight),
|
||||
);
|
||||
} else {
|
||||
i.scrollbarYActive = false;
|
||||
}
|
||||
|
||||
if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) {
|
||||
i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth;
|
||||
}
|
||||
if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) {
|
||||
i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight;
|
||||
}
|
||||
|
||||
updateCss(element, i);
|
||||
|
||||
if (i.scrollbarXActive) {
|
||||
element.classList.add(cls.state.active('x'));
|
||||
} else {
|
||||
element.classList.remove(cls.state.active('x'));
|
||||
i.scrollbarXWidth = 0;
|
||||
i.scrollbarXLeft = 0;
|
||||
element.scrollLeft = i.isRtl === true ? i.contentWidth : 0;
|
||||
}
|
||||
if (i.scrollbarYActive) {
|
||||
element.classList.add(cls.state.active('y'));
|
||||
} else {
|
||||
element.classList.remove(cls.state.active('y'));
|
||||
i.scrollbarYHeight = 0;
|
||||
i.scrollbarYTop = 0;
|
||||
element.scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function getThumbSize(i, thumbSize) {
|
||||
if (i.settings.minScrollbarLength) {
|
||||
thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength);
|
||||
}
|
||||
if (i.settings.maxScrollbarLength) {
|
||||
thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength);
|
||||
}
|
||||
return thumbSize;
|
||||
}
|
||||
|
||||
function updateCss(element, i) {
|
||||
const xRailOffset = { width: i.railXWidth };
|
||||
const roundedScrollTop = Math.floor(element.scrollTop);
|
||||
|
||||
if (i.isRtl) {
|
||||
xRailOffset.left =
|
||||
i.negativeScrollAdjustment +
|
||||
element.scrollLeft +
|
||||
i.containerWidth -
|
||||
i.contentWidth;
|
||||
} else {
|
||||
xRailOffset.left = element.scrollLeft;
|
||||
}
|
||||
if (i.isScrollbarXUsingBottom) {
|
||||
xRailOffset.bottom = i.scrollbarXBottom - roundedScrollTop;
|
||||
} else {
|
||||
xRailOffset.top = i.scrollbarXTop + roundedScrollTop;
|
||||
}
|
||||
CSS.set(i.scrollbarXRail, xRailOffset);
|
||||
|
||||
const yRailOffset = { top: roundedScrollTop, height: i.railYHeight };
|
||||
if (i.isScrollbarYUsingRight) {
|
||||
if (i.isRtl) {
|
||||
yRailOffset.right =
|
||||
i.contentWidth -
|
||||
(i.negativeScrollAdjustment + element.scrollLeft) -
|
||||
i.scrollbarYRight -
|
||||
i.scrollbarYOuterWidth -
|
||||
9;
|
||||
} else {
|
||||
yRailOffset.right = i.scrollbarYRight - element.scrollLeft;
|
||||
}
|
||||
} else {
|
||||
if (i.isRtl) {
|
||||
yRailOffset.left =
|
||||
i.negativeScrollAdjustment +
|
||||
element.scrollLeft +
|
||||
i.containerWidth * 2 -
|
||||
i.contentWidth -
|
||||
i.scrollbarYLeft -
|
||||
i.scrollbarYOuterWidth;
|
||||
} else {
|
||||
yRailOffset.left = i.scrollbarYLeft + element.scrollLeft;
|
||||
}
|
||||
}
|
||||
CSS.set(i.scrollbarYRail, yRailOffset);
|
||||
|
||||
CSS.set(i.scrollbarX, {
|
||||
left: i.scrollbarXLeft,
|
||||
width: i.scrollbarXWidth - i.railBorderXWidth,
|
||||
});
|
||||
CSS.set(i.scrollbarY, {
|
||||
top: i.scrollbarYTop,
|
||||
height: i.scrollbarYHeight - i.railBorderYWidth,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
LabelService,
|
||||
useIDEService,
|
||||
useStyling,
|
||||
} from '@coze-project-ide/core';
|
||||
|
||||
import { type StatusBarItem } from '../../types/view';
|
||||
|
||||
interface StatusBarProps {
|
||||
items: StatusBarItem[];
|
||||
}
|
||||
|
||||
const StatusBarItem: React.FC<{ item: StatusBarItem }> = ({ item }) => {
|
||||
const labelService = useIDEService<LabelService>(LabelService);
|
||||
const label = useMemo(() => labelService.renderer(item.uri), [item.uri]);
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
return <div className="flowide-status-bar-item">{label}</div>;
|
||||
};
|
||||
|
||||
const StatusBar: React.FC<StatusBarProps> = ({ items }) => {
|
||||
useStyling(
|
||||
'flowide-status-bar-widget',
|
||||
(_, { getColor }) => `
|
||||
.flowide-status-bar-widget-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.flowide-status-bar-side {
|
||||
display: flex;
|
||||
}
|
||||
.flowide-status-bar-item {
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
margin: 0 2px;
|
||||
font-size: 12px;
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.flowide-status-bar-item:hover {
|
||||
background: ${getColor('flowide.color.base.fill.0')}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flowide-status-bar-widget-container">
|
||||
<div className="flowide-status-bar-side">
|
||||
{items
|
||||
.filter(item => item.position === 'left')
|
||||
.map(item => (
|
||||
<StatusBarItem item={item} key={item.uri.toString()} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flowide-status-bar-side">
|
||||
{items
|
||||
.filter(item => item.position === 'right')
|
||||
.map(item => (
|
||||
<StatusBarItem item={item} key={item.uri.toString()} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { StatusBar };
|
||||
19
frontend/packages/project-ide/view/src/constants/area-id.ts
Normal file
19
frontend/packages/project-ide/view/src/constants/area-id.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const MAIN_PANEL_ID = 'flowide-main-panel';
|
||||
|
||||
export const BOTTOM_PANEL_ID = 'flowide-bottom-panel';
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
export const CUSTOM_TAB_BAR_CONTAINER = 'custom-tabBar-container';
|
||||
|
||||
// 前边的 action
|
||||
export const PRE_TAB_BAR_ACTION_CONTAINER = 'pre-flow-tabBar-action-container';
|
||||
// 后边的 action
|
||||
export const TAB_BAR_ACTION_CONTAINER = 'flow-tabBar-action-container';
|
||||
// tab bar 滚动区域
|
||||
export const TAB_BAR_SCROLL_CONTAINER = 'flow-tabBar-scroll-container';
|
||||
|
||||
// toolbar
|
||||
export const TAB_BAR_TOOLBAR = 'flow-toolbar-container';
|
||||
export const TAB_BAR_TOOLBAR_ITEM = 'flow-toolbar-item';
|
||||
export const DISABLE_HANDLE_EVENT = 'disable-handle-event';
|
||||
|
||||
/** DebugBar 可拖拽区域 classname */
|
||||
export const DEBUG_BAR_DRAGGABLE = 'flow-debug-bar-draggable';
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const SINGLE_MODE = 'single-document';
|
||||
19
frontend/packages/project-ide/view/src/constants/hover.ts
Normal file
19
frontend/packages/project-ide/view/src/constants/hover.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { URI } from '@coze-project-ide/core';
|
||||
|
||||
export const HOVER_TOOLTIP_LABEL = new URI('flowide:///hover/tooltip-label');
|
||||
21
frontend/packages/project-ide/view/src/constants/index.ts
Normal file
21
frontend/packages/project-ide/view/src/constants/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { VIEW_CONTAINER_CLASS_NAME, PANEL_CLASS_NAME_MAP } from './view';
|
||||
export * from './class-names';
|
||||
export * from './hover';
|
||||
export * from './area-id';
|
||||
export { SINGLE_MODE } from './dock-panel';
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type ViewPluginOptions } from '../types';
|
||||
export const ViewOptions = Symbol('ViewOptions');
|
||||
export type ViewOptions = ViewPluginOptions;
|
||||
54
frontend/packages/project-ide/view/src/constants/view.ts
Normal file
54
frontend/packages/project-ide/view/src/constants/view.ts
Normal 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.
|
||||
*/
|
||||
|
||||
import { URI } from '@coze-project-ide/core';
|
||||
|
||||
import { LayoutPanelType } from '../types';
|
||||
import { BOTTOM_PANEL_ID, MAIN_PANEL_ID } from './area-id';
|
||||
|
||||
export const VIEW_CONTAINER_CLASS_NAME = 'flowide-container';
|
||||
|
||||
export const PANEL_CLASS_NAME_MAP: Record<LayoutPanelType, URI> = {
|
||||
[LayoutPanelType.TOP_BAR]: new URI('flowide:///panel/flowide-top-bar'),
|
||||
[LayoutPanelType.ACTIVITY_BAR]: new URI(
|
||||
'flowide:///panel/flowide-activity-bar',
|
||||
),
|
||||
[LayoutPanelType.PRIMARY_SIDEBAR]: new URI(
|
||||
'flowide:///panel/flowide-primary-sidebar',
|
||||
),
|
||||
[LayoutPanelType.MAIN_PANEL]: new URI(`flowide:///panel/${MAIN_PANEL_ID}`),
|
||||
[LayoutPanelType.SECONDARY_SIDEBAR]: new URI(
|
||||
'flowide:///panel/flowide-secondary-sidebar',
|
||||
),
|
||||
[LayoutPanelType.BOTTOM_PANEL]: new URI(
|
||||
`flowide:///panel/${BOTTOM_PANEL_ID}`,
|
||||
),
|
||||
[LayoutPanelType.STATUS_BAR]: new URI('flowide:///panel/flowide-status-bar'),
|
||||
[LayoutPanelType.RIGHT_BAR]: new URI('flowide:///panel/flowide-right-bar'),
|
||||
};
|
||||
|
||||
export const ALL_PANEL_TYPES = [
|
||||
LayoutPanelType.TOP_BAR,
|
||||
LayoutPanelType.ACTIVITY_BAR,
|
||||
LayoutPanelType.PRIMARY_SIDEBAR,
|
||||
LayoutPanelType.MAIN_PANEL,
|
||||
LayoutPanelType.SECONDARY_SIDEBAR,
|
||||
LayoutPanelType.BOTTOM_PANEL,
|
||||
LayoutPanelType.STATUS_BAR,
|
||||
LayoutPanelType.RIGHT_BAR,
|
||||
];
|
||||
|
||||
export const SPLIT_PANEL_CLASSNAME = 'flow-split-widget-panel';
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { type interfaces } from 'inversify';
|
||||
import { type URI } from '@coze-project-ide/core';
|
||||
|
||||
import { WidgetFactory } from '../widget/widget-factory';
|
||||
import { STATUS_BAR_CONTENT } from '../widget/react-widgets/status-bar-widget';
|
||||
import { ACTIVITY_BAR_CONTENT } from '../widget/react-widgets/activity-bar-widget';
|
||||
import { ActivityBarWidget, StatusBarWidget } from '../widget/react-widgets';
|
||||
import { LayoutPanelType } from '../types';
|
||||
|
||||
export const bindActivityBarView = (bind: interfaces.Bind): void => {
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||
area: LayoutPanelType.ACTIVITY_BAR,
|
||||
canHandle: (uri: URI) => uri.isEqualOrParent(ACTIVITY_BAR_CONTENT),
|
||||
createWidget: () => {
|
||||
const childContainer = container.createChild();
|
||||
childContainer.bind(ActivityBarWidget).toSelf().inSingletonScope();
|
||||
|
||||
return childContainer.get(ActivityBarWidget);
|
||||
},
|
||||
}));
|
||||
bind(WidgetFactory).toDynamicValue(({ container }) => ({
|
||||
area: LayoutPanelType.STATUS_BAR,
|
||||
canHandle: (uri: URI) => uri.isEqualOrParent(STATUS_BAR_CONTENT),
|
||||
createWidget: () => {
|
||||
const childContainer = container.createChild();
|
||||
childContainer.bind(StatusBarWidget).toSelf().inSingletonScope();
|
||||
|
||||
return childContainer.get(StatusBarWidget);
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ContainerModule } from 'inversify';
|
||||
import { bindContributions } from '@flowgram-adapter/common';
|
||||
import { LifecycleContribution } from '@coze-project-ide/core';
|
||||
|
||||
import { MenuService, MenuRegistry } from './menu-registry';
|
||||
import { Menu, MenuFactory } from './menu';
|
||||
import { ContextMenu } from './context-menu';
|
||||
|
||||
export const ContextMenuContainerModule = new ContainerModule(bind => {
|
||||
bind(MenuService).toService(MenuRegistry);
|
||||
bindContributions(bind, MenuRegistry, [LifecycleContribution]);
|
||||
|
||||
bind(MenuFactory).toFactory(context => () => {
|
||||
const container = context.container.createChild();
|
||||
container.bind(Menu).toSelf().inSingletonScope();
|
||||
return container.get(Menu);
|
||||
});
|
||||
bind(ContextMenu).toSelf().inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import { inject, injectable, postConstruct } from 'inversify';
|
||||
import { Disposable } from '@flowgram-adapter/common';
|
||||
|
||||
import { Selector } from '../../lumino/domutils';
|
||||
import { ArrayExt } from '../../lumino/algorithm';
|
||||
import { Menu, MenuFactory } from './menu';
|
||||
|
||||
export type CanHandle = string | ((command?: string) => boolean);
|
||||
|
||||
/**
|
||||
* 全局 contextmenu 监听
|
||||
*/
|
||||
@injectable()
|
||||
export class ContextMenu {
|
||||
@inject(MenuFactory) menuFactory: MenuFactory;
|
||||
|
||||
// 全局主菜单
|
||||
menu: Menu;
|
||||
|
||||
@postConstruct()
|
||||
init() {
|
||||
this.menu = this.menuFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除项
|
||||
*/
|
||||
deleteItem(canHandle: CanHandle) {
|
||||
if (typeof canHandle === 'string') {
|
||||
// 精确删除
|
||||
const item = this._items.find(i => i.command === canHandle);
|
||||
ArrayExt.removeFirstOf(this._items, item);
|
||||
} else {
|
||||
// 模糊删除
|
||||
this._items.forEach(i => {
|
||||
if (canHandle(i.command)) {
|
||||
ArrayExt.removeFirstOf(this._items, i);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加项
|
||||
*/
|
||||
addItem(options: ContextMenu.IItemOptions): Disposable {
|
||||
const item = Private.createItem(options, this._idTick++);
|
||||
|
||||
this._items.push(item);
|
||||
|
||||
return Disposable.create(() => {
|
||||
ArrayExt.removeFirstOf(this._items, item);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动关闭 menu
|
||||
*/
|
||||
close() {
|
||||
this.menu.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开事件
|
||||
*/
|
||||
open(event: React.MouseEvent, args?: any): boolean {
|
||||
Menu.saveWindowData();
|
||||
|
||||
this.menu.clearItems();
|
||||
|
||||
if (this._items.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const items = Private.matchItems(
|
||||
this._items,
|
||||
event,
|
||||
this._groupByTarget,
|
||||
this._sortBySelector,
|
||||
);
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
if (args) {
|
||||
item.args = args;
|
||||
}
|
||||
if (item.filter && !item.filter(args)) {
|
||||
continue;
|
||||
}
|
||||
this.menu.addItem(item);
|
||||
}
|
||||
|
||||
this.menu.open(event.clientX, event.clientY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _groupByTarget = true;
|
||||
|
||||
private _idTick = 0;
|
||||
|
||||
private _items: Private.IItem[] = [];
|
||||
|
||||
private _sortBySelector = true;
|
||||
}
|
||||
|
||||
export namespace ContextMenu {
|
||||
export interface IOptions {
|
||||
renderer?: Menu.IRenderer;
|
||||
|
||||
sortBySelector?: boolean;
|
||||
|
||||
groupByTarget?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* An options object for creating a context menu item.
|
||||
*/
|
||||
export interface IItemOptions extends Menu.IItemOptions {
|
||||
/**
|
||||
* The CSS selector for the context menu item.
|
||||
*
|
||||
* 只有当当前元素冒泡途径 selector 元素,才会触发这个 contextmenu 事件。
|
||||
* 底层通过 querySelector 获取,需要加上 commas
|
||||
*/
|
||||
selector: string;
|
||||
|
||||
/**
|
||||
* The default rank is `Infinity`.
|
||||
*/
|
||||
rank?: number;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Private {
|
||||
export interface IItem extends Menu.IItemOptions {
|
||||
selector: string;
|
||||
|
||||
rank: number;
|
||||
|
||||
id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a normalized context menu item from an options object.
|
||||
*/
|
||||
export function createItem(
|
||||
options: ContextMenu.IItemOptions,
|
||||
id: number,
|
||||
): IItem {
|
||||
const selector = validateSelector(options.selector);
|
||||
const rank = options.rank !== undefined ? options.rank : Infinity;
|
||||
return { ...options, selector, rank, id };
|
||||
}
|
||||
|
||||
export function matchItems(
|
||||
items: IItem[],
|
||||
event: React.MouseEvent,
|
||||
groupByTarget: boolean,
|
||||
sortBySelector: boolean,
|
||||
): IItem[] | null {
|
||||
let target = event.target as Element | null;
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentTarget = event.currentTarget as Element | null;
|
||||
|
||||
if (!currentTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: IItem[] = [];
|
||||
|
||||
const availableItems: Array<IItem | null> = items.slice();
|
||||
|
||||
while (target !== null) {
|
||||
const matches: IItem[] = [];
|
||||
|
||||
for (let i = 0, n = availableItems.length; i < n; ++i) {
|
||||
const item = availableItems[i];
|
||||
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Selector.matches(target, item.selector)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
matches.push(item);
|
||||
|
||||
availableItems[i] = null;
|
||||
}
|
||||
|
||||
if (matches.length !== 0) {
|
||||
if (groupByTarget) {
|
||||
matches.sort(sortBySelector ? itemCmp : itemCmpRank);
|
||||
}
|
||||
result.push(...matches);
|
||||
}
|
||||
|
||||
if (target === currentTarget) {
|
||||
break;
|
||||
}
|
||||
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
if (!groupByTarget) {
|
||||
result.sort(sortBySelector ? itemCmp : itemCmpRank);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateSelector(selector: string): string {
|
||||
if (selector.indexOf(',') !== -1) {
|
||||
throw new Error(`Selector cannot contain commas: ${selector}`);
|
||||
}
|
||||
if (!Selector.isValid(selector)) {
|
||||
throw new Error(`Invalid selector: ${selector}`);
|
||||
}
|
||||
return selector;
|
||||
}
|
||||
|
||||
function itemCmpRank(a: IItem, b: IItem): number {
|
||||
const r1 = a.rank;
|
||||
const r2 = b.rank;
|
||||
if (r1 !== r2) {
|
||||
return r1 < r2 ? -1 : 1; // Infinity-safe
|
||||
}
|
||||
|
||||
return a.id - b.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* A sort comparison function for a context menu item by selectors and ranks.
|
||||
*/
|
||||
function itemCmp(a: IItem, b: IItem): number {
|
||||
const s1 = Selector.calculateSpecificity(a.selector);
|
||||
const s2 = Selector.calculateSpecificity(b.selector);
|
||||
if (s1 !== s2) {
|
||||
return s2 - s1;
|
||||
}
|
||||
|
||||
return itemCmpRank(a, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { definePluginCreator } from '@coze-project-ide/core';
|
||||
|
||||
import { ContextMenuContainerModule } from './context-menu-container-module';
|
||||
|
||||
export const createContextMenuPlugin = definePluginCreator<void>({
|
||||
containerModules: [ContextMenuContainerModule],
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createContextMenuPlugin } from './create-context-menu-plugin';
|
||||
export { MenuService } from './menu-registry';
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import { type LifecycleContribution } from '@coze-project-ide/core';
|
||||
|
||||
import { type Menu, MenuFactory } from './menu';
|
||||
import { type CanHandle, ContextMenu } from './context-menu';
|
||||
|
||||
export const MenuService = Symbol('MenuService');
|
||||
|
||||
/**
|
||||
* menu service 注册
|
||||
*/
|
||||
export interface MenuService {
|
||||
addMenuItem: (options: ContextMenu.IItemOptions) => void;
|
||||
|
||||
createSubMenu: () => Menu;
|
||||
|
||||
addSubMenuItem: (submenu: Menu, options: Menu.IItemOptions) => void;
|
||||
|
||||
open: (event: React.MouseEvent, args?: any) => boolean;
|
||||
|
||||
clearMenuItems: (canHandles: CanHandle[]) => void;
|
||||
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MenuRegistry implements MenuService, LifecycleContribution {
|
||||
@inject(ContextMenu) contextMenu: ContextMenu;
|
||||
|
||||
@inject(MenuFactory) menuFactory: MenuFactory;
|
||||
|
||||
onInit() {}
|
||||
|
||||
clearMenuItems(canHandles: CanHandle[]) {
|
||||
canHandles.forEach(handle => {
|
||||
this.contextMenu.deleteItem(handle);
|
||||
});
|
||||
}
|
||||
|
||||
clearMenuItem(canHandle: string | ((command: string) => boolean)) {
|
||||
if (typeof canHandle === 'string') {
|
||||
}
|
||||
}
|
||||
|
||||
addMenuItem(options: ContextMenu.IItemOptions): void {
|
||||
this.contextMenu.addItem(options);
|
||||
}
|
||||
|
||||
createSubMenu(): Menu {
|
||||
const submenu = this.menuFactory();
|
||||
return submenu;
|
||||
}
|
||||
|
||||
addSubMenuItem(submenu: Menu, options: Menu.IItemOptions): void {
|
||||
submenu.addItem(options);
|
||||
}
|
||||
|
||||
open(event: React.MouseEvent, args?: any): boolean {
|
||||
return this.contextMenu.open(event, args);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.contextMenu.close();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { bindActivityBarView } from './activity-bar-contribution';
|
||||
export { ViewCommonContribution } from './view-common-contribution';
|
||||
export { ViewContribution } from './view-contribution';
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const getFlowMenuStyle = (getColor: (id: string) => string): string => `
|
||||
.flow-Menu {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 4px;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
outline: none;
|
||||
font: 12px Helvetica,Arial,sans-serif;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
background: ${getColor('flowide.color.base.bg.0')};
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
border: 1px solid ${getColor('flowide.color.menu.border')};
|
||||
box-shadow: 0 1px 6px ${getColor('flowide.color.menu.box.shadow')};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.flow-Menu-content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
outline: none;
|
||||
flex-direction: column;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.flow-Menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 4px;
|
||||
border-radius: 4px;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flow-Menu-item.flow-mod-active {
|
||||
background: ${getColor('flowide.color.base.fill.0')};
|
||||
}
|
||||
|
||||
.flow-Menu-item.flow-mod-disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.flow-Menu-item.flow-mod-hidden,
|
||||
.flow-Menu-item.flow-mod-collapsed {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.flow-Menu-itemIcon,
|
||||
.flow-Menu-itemSubmenuIcon {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.flow-Menu-itemLabel {
|
||||
text-align: left;
|
||||
padding: 4px 35px 4px 2px;
|
||||
}
|
||||
|
||||
.flow-Menu-itemShortcut {
|
||||
text-align: right;
|
||||
}
|
||||
.flow-Menu-itemIcon::before,
|
||||
.flow-Menu-itemSubmenuIcon::before {
|
||||
font-family: codicon;
|
||||
}
|
||||
|
||||
.flow-Menu-item > .flow-Menu-itemSubmenuIcon::before {
|
||||
content: '\\eab6';
|
||||
line-height: 20px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { LayoutPanelType } from '../../types';
|
||||
import { SPLIT_PANEL_CLASSNAME } from '../../constants/view';
|
||||
import { PANEL_CLASS_NAME_MAP } from '../../constants';
|
||||
|
||||
export const getPanelStyle = (getColor: (id: string) => string): string => `
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.TOP_BAR].displayName} {
|
||||
min-height: 40px;
|
||||
background: ${getColor('flowide.color.base.bg.2')};
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
border-bottom: 1px solid ${getColor('flowide.color.base.border')};
|
||||
}
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.ACTIVITY_BAR].displayName} {
|
||||
min-width: 36px;
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
background: ${getColor('flowide.color.base.bg.2')};
|
||||
border-right: 1px solid ${getColor('flowide.color.base.border')};
|
||||
}
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.PRIMARY_SIDEBAR].displayName} {
|
||||
min-width: 110px !important;
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
background: ${getColor('flowide.color.base.bg.1')};
|
||||
border-right: 1px solid ${getColor('flowide.color.base.border')};
|
||||
}
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.MAIN_PANEL].displayName} {
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
background: ${getColor('flowide.color.base.bg.0')};
|
||||
}
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.SECONDARY_SIDEBAR].displayName} {
|
||||
min-width: 110px;
|
||||
}
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.STATUS_BAR].displayName} {
|
||||
min-height: 22px;
|
||||
background: ${getColor('flowide.color.base.bg.2')};
|
||||
border-top: 1px solid ${getColor('flowide.color.base.border')};
|
||||
}
|
||||
#${PANEL_CLASS_NAME_MAP[LayoutPanelType.BOTTOM_PANEL].displayName} {
|
||||
background: ${getColor('flowide.color.base.bg.0')};
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
border-top: 1px solid ${getColor('flowide.color.base.border')};
|
||||
}
|
||||
|
||||
.${SPLIT_PANEL_CLASSNAME} .lm-SplitPanel-child.expand {
|
||||
min-height: 75px;
|
||||
}
|
||||
|
||||
.${SPLIT_PANEL_CLASSNAME} .lm-SplitPanel-child.close {
|
||||
min-height: 22px;
|
||||
max-height: 22px;
|
||||
}
|
||||
|
||||
.${SPLIT_PANEL_CLASSNAME}[data-orientation="vertical"] .lm-SplitPanel-handle {
|
||||
background: ${getColor('flowide.color.base.border')};
|
||||
min-height: 1px;
|
||||
z-index: 3;
|
||||
}
|
||||
.${SPLIT_PANEL_CLASSNAME}[data-orientation="vertical"] .lm-SplitPanel-handle:hover {
|
||||
background: ${getColor('flowide.color.base.primary.hover')};
|
||||
min-height: 4px;
|
||||
}
|
||||
.${SPLIT_PANEL_CLASSNAME}[data-orientation="vertical"] .lm-SplitPanel-handle:active {
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.${SPLIT_PANEL_CLASSNAME}[data-orientation="horizontal"] .lm-SplitPanel-handle {
|
||||
background: ${getColor('flowide.color.base.border')};
|
||||
min-width: 1px;
|
||||
}
|
||||
.${SPLIT_PANEL_CLASSNAME}[data-orientation="horizontal"] .lm-SplitPanel-handle:hover {
|
||||
background: ${getColor('flowide.color.base.primary.hover')};
|
||||
min-width: 4px;
|
||||
}
|
||||
.${SPLIT_PANEL_CLASSNAME}[data-orientation="horizontal"] .lm-SplitPanel-handle:active {
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
min-width: 4px;
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { inject, injectable } from 'inversify';
|
||||
import {
|
||||
type CommandContribution,
|
||||
type CommandRegistry,
|
||||
type ShortcutsContribution,
|
||||
type StylingContribution,
|
||||
type Collector,
|
||||
type ColorTheme,
|
||||
type ShortcutsRegistry,
|
||||
CommandRegistryFactory,
|
||||
Command,
|
||||
NavigationService,
|
||||
} from '@coze-project-ide/core';
|
||||
|
||||
import { WidgetOpenHandler } from '../widget/widget-open-handler';
|
||||
import { type FlowDockPanel } from '../widget/dock-panel';
|
||||
import { type CustomTitleType } from '../types';
|
||||
import { ApplicationShell } from '../shell';
|
||||
import { ViewService } from '../services/view-service';
|
||||
import {
|
||||
BOTTOM_PANEL_ID,
|
||||
CUSTOM_TAB_BAR_CONTAINER,
|
||||
MAIN_PANEL_ID,
|
||||
TAB_BAR_ACTION_CONTAINER,
|
||||
TAB_BAR_SCROLL_CONTAINER,
|
||||
TAB_BAR_TOOLBAR,
|
||||
TAB_BAR_TOOLBAR_ITEM,
|
||||
} from '../constants';
|
||||
import { getPanelStyle } from './styles/panel-style';
|
||||
import { getFlowMenuStyle } from './styles/menu-style';
|
||||
|
||||
@injectable()
|
||||
export class ViewCommonContribution
|
||||
implements CommandContribution, StylingContribution, ShortcutsContribution
|
||||
{
|
||||
@inject(ApplicationShell) protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(ViewService) viewService: ViewService;
|
||||
|
||||
@inject(NavigationService) navigationService: NavigationService;
|
||||
|
||||
@inject(CommandRegistryFactory) commandFactory: () => CommandRegistry;
|
||||
|
||||
get commandService(): CommandRegistry {
|
||||
return this.commandFactory();
|
||||
}
|
||||
|
||||
@inject(WidgetOpenHandler) protected readonly openHandler: WidgetOpenHandler;
|
||||
|
||||
registerStyle({ add }: Collector, { getColor }: ColorTheme): void {
|
||||
add(`
|
||||
${getFlowMenuStyle(getColor)}
|
||||
.flow-hover {
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
background: ${getColor('flowide.color.base.bg.0')};
|
||||
|
||||
border: 2px solid ${getColor('flowide.color.base.border')};
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
.flowide-container {
|
||||
height: 100%;
|
||||
}
|
||||
.flowide-container .debug-bar-widget-container {
|
||||
position: fixed;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
}
|
||||
.flowide-container .lm-Widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.flowide-container .flow-tab-icon-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.flowide-container .flow-tab-icon-label .flow-TabBar-tabLabel-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.flowide-container .lm-TabBar-tabCloseIcon:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.flowide-container .lm-TabBar-content {
|
||||
width: fit-content;
|
||||
border: 0;
|
||||
}
|
||||
.flowide-container .lm-DockPanel-handle {
|
||||
background: ${getColor('flowide.color.base.border')};
|
||||
}
|
||||
.flowide-container .lm-TabBar {
|
||||
color: ${getColor('flowide.color.base.text.2')};
|
||||
background: ${getColor('flowide.color.base.fill.0')};
|
||||
display: flex;
|
||||
height: 24px;
|
||||
}
|
||||
.flowide-container .lm-TabBar-tab {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
min-width: 125px;
|
||||
max-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.flowide-container .lm-TabBar-tab:hover {
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
background: ${getColor('flowide.color.base.fill.0')};
|
||||
}
|
||||
.flowide-container .lm-DockPanel-handle[data-orientation="vertical"] {
|
||||
min-height: 1px;
|
||||
z-index: 3;
|
||||
}
|
||||
.flowide-container .lm-DockPanel-handle[data-orientation="vertical"]:hover {
|
||||
background: ${getColor('flowide.color.base.primary.hover')};
|
||||
min-height: 4px;
|
||||
}
|
||||
.flowide-container .lm-DockPanel-handle[data-orientation="vertical"]:active {
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
min-height: 4px;
|
||||
}
|
||||
|
||||
.flowide-container .lm-DockPanel-handle[data-orientation="horizontal"] {
|
||||
min-width: 1px;
|
||||
}
|
||||
.flowide-container .lm-DockPanel-handle[data-orientation="horizontal"]:hover {
|
||||
background: ${getColor('flowide.color.base.primary.hover')};
|
||||
min-width: 4px;
|
||||
}
|
||||
.flowide-container .lm-DockPanel-handle[data-orientation="horizontal"]:active {
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
min-width: 4px;
|
||||
}
|
||||
.flowide-container .lm-TabBar-tab.lm-mod-current {
|
||||
background: ${getColor('flowide.color.base.bg.0')};
|
||||
color: ${getColor('flowide.color.base.text.0')};
|
||||
transform: unset;
|
||||
position: relative;
|
||||
}
|
||||
.flowide-container #${MAIN_PANEL_ID} .lm-TabBar-tab.lm-mod-current::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
}
|
||||
.flowide-container #${BOTTOM_PANEL_ID} .lm-TabBar-tab.lm-mod-current::before {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: ${getColor('flowide.color.base.primary')};
|
||||
}
|
||||
.flowide-container .lm-TabBar-tabCloseIcon.saving:before {
|
||||
content: "\\f111";
|
||||
}
|
||||
.flowide-container .lm-TabBar-tabCloseIcon.saving:hover:before {
|
||||
content: "\\f00d";
|
||||
}
|
||||
.flowide-container .${CUSTOM_TAB_BAR_CONTAINER} {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.flowide-container .${TAB_BAR_SCROLL_CONTAINER} {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.flowide-container .${TAB_BAR_SCROLL_CONTAINER} .ide-ps__rail-x {
|
||||
z-index: 999999;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
.flowide-container .${TAB_BAR_SCROLL_CONTAINER} .ide-ps__thumb-x {
|
||||
height: 2px;
|
||||
}
|
||||
.flowide-container .${TAB_BAR_ACTION_CONTAINER} {
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
}
|
||||
.flowide-container .${TAB_BAR_ACTION_CONTAINER} .${TAB_BAR_TOOLBAR} {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
.flowide-container .${TAB_BAR_ACTION_CONTAINER} .${TAB_BAR_TOOLBAR_ITEM} {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 4px;
|
||||
}
|
||||
${getPanelStyle(getColor)}
|
||||
}`);
|
||||
}
|
||||
|
||||
registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_CLOSE_ALL_WIDGET,
|
||||
label: 'Close All Tab',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
const parentWidget = this.shell.currentWidget?.parent;
|
||||
const widgets = (parentWidget as FlowDockPanel).tabBars();
|
||||
|
||||
let titles: CustomTitleType[] = [];
|
||||
for (const customTabBar of widgets) {
|
||||
titles = titles.concat(customTabBar.titles as CustomTitleType[]);
|
||||
}
|
||||
this.commandService.executeCommand(
|
||||
Command.Default.VIEW_SAVING_WIDGET_CLOSE_CONFIRM,
|
||||
titles,
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_CLOSE_CURRENT_WIDGET,
|
||||
label: 'Close Current Tab',
|
||||
shortLabel: 'Close',
|
||||
},
|
||||
{
|
||||
execute: widget => {
|
||||
const closeWidget = widget || this.shell.currentWidget;
|
||||
if (closeWidget) {
|
||||
this.commandService.executeCommand(
|
||||
Command.Default.VIEW_SAVING_WIDGET_CLOSE_CONFIRM,
|
||||
[closeWidget.title],
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_REOPEN_LAST_WIDGET,
|
||||
label: 'Reopen Last Tab',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
const lastCloseUri = this.shell.closeWidgetUriStack.pop();
|
||||
if (lastCloseUri) {
|
||||
this.openHandler.open(lastCloseUri);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_CLOSE_OTHER_WIDGET,
|
||||
label: 'Close Other Tab',
|
||||
},
|
||||
{
|
||||
execute: widget => {
|
||||
try {
|
||||
const currentWidget = widget || this.shell.currentWidget;
|
||||
const parentWidget = currentWidget?.parent;
|
||||
if (!parentWidget) {
|
||||
return;
|
||||
}
|
||||
const titles: CustomTitleType[] = [];
|
||||
const widgets = (parentWidget as FlowDockPanel).tabBars();
|
||||
|
||||
for (const customTabBar of widgets) {
|
||||
[...customTabBar.titles].map(title => {
|
||||
if (title.owner !== currentWidget) {
|
||||
titles.push(title as CustomTitleType);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.commandService.executeCommand(
|
||||
Command.Default.VIEW_SAVING_WIDGET_CLOSE_CONFIRM,
|
||||
titles,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_CLOSE_BOTTOM_PANEL,
|
||||
label: 'Close/Open Bottom Pannel',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
// 没有 focus 内容的时候默认打开 problem
|
||||
if (!this.shell.bottomPanel?.currentTitle) {
|
||||
this.commandService.executeCommand(Command.Default.VIEW_PROBLEMS);
|
||||
}
|
||||
|
||||
this.shell.bottomPanel.setHidden(!this.shell.bottomPanel.isHidden);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_OPEN_NEXT_TAB,
|
||||
label: 'Open Next Tab',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
this.viewService.openNextTab();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_OEPN_LAST_TAB,
|
||||
label: 'Open Last Tab',
|
||||
},
|
||||
{
|
||||
execute: () => {
|
||||
this.viewService.openLastTab();
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
commands.registerCommand(
|
||||
{
|
||||
id: Command.Default.VIEW_FULL_SCREEN,
|
||||
label: 'Full Screen',
|
||||
},
|
||||
{
|
||||
execute: this.shell.disableFullScreen
|
||||
? () => null
|
||||
: () => {
|
||||
this.viewService.switchFullScreenMode();
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
registerShortcuts(registry: ShortcutsRegistry): void {
|
||||
// 关闭当前所有 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'alt shift w',
|
||||
commandId: Command.Default.VIEW_CLOSE_ALL_WIDGET,
|
||||
});
|
||||
|
||||
// 打开下一个 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'alt shift rightarrow',
|
||||
commandId: Command.Default.VIEW_OPEN_NEXT_TAB,
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
// 打开上一个 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'alt shift leftarrow',
|
||||
commandId: Command.Default.VIEW_OEPN_LAST_TAB,
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
// 关闭当前 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'alt w',
|
||||
commandId: Command.Default.VIEW_CLOSE_CURRENT_WIDGET,
|
||||
});
|
||||
|
||||
// 打开刚刚关闭当前 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'alt shift t',
|
||||
commandId: Command.Default.VIEW_REOPEN_LAST_WIDGET,
|
||||
});
|
||||
|
||||
// 关闭除了当前打开的 tab 以外的所有 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'meta alt t',
|
||||
commandId: Command.Default.VIEW_CLOSE_OTHER_WIDGET,
|
||||
});
|
||||
|
||||
// 关闭除了当前打开的 tab 以外的所有 tab
|
||||
registry.registerHandlers({
|
||||
keybinding: 'meta j',
|
||||
commandId: Command.Default.VIEW_CLOSE_BOTTOM_PANEL,
|
||||
});
|
||||
|
||||
// 全屏模式
|
||||
registry.registerHandlers({
|
||||
keybinding: 'alt f',
|
||||
commandId: Command.Default.VIEW_FULL_SCREEN,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { type ViewPluginOptions } from '../types/view';
|
||||
|
||||
export interface ViewOptionRegisterService {
|
||||
register: (options: Partial<ViewPluginOptions>) => void;
|
||||
}
|
||||
|
||||
interface ViewContribution {
|
||||
registerView(service: ViewOptionRegisterService): void;
|
||||
}
|
||||
|
||||
const ViewContribution = Symbol('ViewContribution');
|
||||
|
||||
export { ViewContribution };
|
||||
217
frontend/packages/project-ide/view/src/create-view-plugin.tsx
Normal file
217
frontend/packages/project-ide/view/src/create-view-plugin.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { type interfaces } from 'inversify';
|
||||
|
||||
// import { bindContributionProvider } from '@flowgram-adapter/common';
|
||||
import {
|
||||
bindContributionProvider,
|
||||
bindContributions,
|
||||
} from '@flowgram-adapter/common';
|
||||
import {
|
||||
CommandContribution,
|
||||
definePluginCreator,
|
||||
IDERendererProvider,
|
||||
OpenHandler,
|
||||
ShortcutsContribution,
|
||||
StylingContribution,
|
||||
EventService,
|
||||
Command,
|
||||
domEditable,
|
||||
} from '@coze-project-ide/core';
|
||||
|
||||
import { WidgetManager } from './widget-manager';
|
||||
import { WidgetOpenHandler } from './widget/widget-open-handler';
|
||||
import { WidgetFactory } from './widget/widget-factory';
|
||||
import { TabBarToolbar, TabBarToolbarFactory } from './widget/tab-bar/toolbar';
|
||||
import {
|
||||
TabBarRenderer,
|
||||
TabBarRendererFactory,
|
||||
} from './widget/tab-bar/tab-renderer';
|
||||
import { CustomTabBar, TabBarFactory } from './widget/tab-bar/custom-tabbar';
|
||||
import { DebugBarWidget } from './widget/react-widgets/debug-bar-widget';
|
||||
import {
|
||||
CustomRenderWidget,
|
||||
CustomRenderWidgetFactory,
|
||||
} from './widget/react-widgets/custom-render-widget-factory';
|
||||
import { type ReactWidget } from './widget/react-widget';
|
||||
import {
|
||||
SidePanelHandler,
|
||||
SidePanelHandlerFactory,
|
||||
} from './widget/handlers/side-panel-handler';
|
||||
import { DockPanelRendererFactory } from './widget/dock-panel-renderer-factory';
|
||||
import { DockPanelRenderer } from './widget/dock-panel-renderer';
|
||||
import { FlowDockPanel } from './widget/dock-panel';
|
||||
import { ViewRenderer } from './view-renderer';
|
||||
import { ViewManager } from './view-manager';
|
||||
import { type ViewPluginOptions, type ToolbarAlign } from './types';
|
||||
import {
|
||||
ApplicationShell,
|
||||
CustomPreferenceContribution,
|
||||
LayoutRestorer,
|
||||
} from './shell';
|
||||
import { ViewService } from './services/view-service';
|
||||
import { HoverService } from './services/hover-service';
|
||||
import { DragService } from './services/drag-service';
|
||||
import { DebugService } from './services/debug-service';
|
||||
import { type DockPanel } from './lumino/widgets';
|
||||
import { MenuService } from './contributions/context-menu';
|
||||
import {
|
||||
bindActivityBarView,
|
||||
ViewCommonContribution,
|
||||
ViewContribution,
|
||||
} from './contributions';
|
||||
import { ViewOptions } from './constants/view-options';
|
||||
import { MAIN_PANEL_ID } from './constants';
|
||||
const DefaultFallbackRender = () => <div>Something went wrong.</div>;
|
||||
|
||||
/**
|
||||
* 点位背景插件
|
||||
*/
|
||||
export const createViewPlugin = definePluginCreator<ViewPluginOptions>({
|
||||
onBind: ({ bind }, opts) => {
|
||||
bind(ViewManager).toSelf().inSingletonScope();
|
||||
bind(WidgetManager).toSelf().inSingletonScope();
|
||||
bind(ViewRenderer).toSelf().inSingletonScope();
|
||||
|
||||
bind(ViewOptions).toConstantValue({
|
||||
widgetFallbackRender: DefaultFallbackRender,
|
||||
...opts,
|
||||
});
|
||||
|
||||
bind(ApplicationShell).toSelf().inSingletonScope();
|
||||
bind(LayoutRestorer).toSelf().inSingletonScope();
|
||||
bindContributionProvider(bind, WidgetFactory);
|
||||
bindContributionProvider(bind, ViewContribution);
|
||||
bindContributionProvider(bind, CustomPreferenceContribution);
|
||||
bindContributions(bind, WidgetOpenHandler, [OpenHandler]);
|
||||
|
||||
bind(HoverService).toSelf().inSingletonScope();
|
||||
bind(DragService).toSelf().inSingletonScope();
|
||||
bind(ViewService).toSelf().inSingletonScope();
|
||||
bind(DebugService).toSelf().inSingletonScope();
|
||||
bind(DebugBarWidget).toSelf().inSingletonScope();
|
||||
|
||||
bind(SidePanelHandlerFactory).toAutoFactory(SidePanelHandler);
|
||||
bind(SidePanelHandler).toSelf();
|
||||
|
||||
bind(CustomRenderWidgetFactory).toFactory(
|
||||
_context => (childContainer: interfaces.Container) => {
|
||||
childContainer.bind(CustomRenderWidget).toSelf().inSingletonScope();
|
||||
return childContainer.get(CustomRenderWidget);
|
||||
},
|
||||
);
|
||||
|
||||
bind(DockPanelRendererFactory).toFactory(context => () => {
|
||||
const childContainer = context.container.createChild();
|
||||
childContainer.bind(DockPanelRenderer).toSelf().inSingletonScope();
|
||||
childContainer.bind(CustomTabBar).toSelf().inSingletonScope();
|
||||
childContainer.bind(TabBarFactory).toFactory(context => () => {
|
||||
const container = context.container.createChild();
|
||||
container.bind(CustomTabBar).toSelf().inSingletonScope();
|
||||
return container.get(CustomTabBar);
|
||||
});
|
||||
childContainer
|
||||
.bind(TabBarToolbarFactory)
|
||||
.toFactory(context => (align?: ToolbarAlign) => {
|
||||
const container = context.container.createChild();
|
||||
container.bind(TabBarToolbar).toSelf().inSingletonScope();
|
||||
const toolbar = container.get(TabBarToolbar);
|
||||
toolbar.initAlign(align);
|
||||
return toolbar;
|
||||
});
|
||||
childContainer.bind(TabBarRendererFactory).toFactory(context => () => {
|
||||
const container = context.container.createChild();
|
||||
container.bind(TabBarRenderer).toSelf().inSingletonScope();
|
||||
return container.get(TabBarRenderer);
|
||||
});
|
||||
return childContainer.get(DockPanelRenderer);
|
||||
});
|
||||
|
||||
bind(IDERendererProvider)
|
||||
.toDynamicValue(ctx => {
|
||||
const shell = ctx.container.get(ApplicationShell);
|
||||
return ctx.container.get(ViewRenderer).toReactComponent(shell);
|
||||
})
|
||||
.inSingletonScope();
|
||||
|
||||
bindContributions(bind, ViewCommonContribution, [
|
||||
CommandContribution,
|
||||
StylingContribution,
|
||||
ShortcutsContribution,
|
||||
]);
|
||||
|
||||
bind(FlowDockPanel.Factory).toFactory(
|
||||
() => (options?: DockPanel.IOptions) => new FlowDockPanel(options),
|
||||
);
|
||||
|
||||
bindActivityBarView(bind);
|
||||
},
|
||||
|
||||
onInit: async (ctx, opts) => {
|
||||
const viewManager = ctx.get<ViewManager>(ViewManager);
|
||||
await viewManager.init(opts);
|
||||
},
|
||||
// 页面渲染完成后 attach dom
|
||||
onLayoutInit: async (ctx, opts) => {
|
||||
// 预设 contextmenu
|
||||
if (!opts.presetConfig?.disableContextMenu) {
|
||||
const menuService = ctx.container.get<MenuService>(MenuService);
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_CLOSE_ALL_WIDGET,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_CLOSE_OTHER_WIDGET,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_FULL_SCREEN,
|
||||
selector: '.lm-TabBar-tab',
|
||||
filter: (widget: ReactWidget) => {
|
||||
const isMainPanel = widget?.parent?.id === MAIN_PANEL_ID;
|
||||
return isMainPanel;
|
||||
},
|
||||
});
|
||||
menuService.addMenuItem({
|
||||
command: Command.Default.VIEW_CLOSE_CURRENT_WIDGET,
|
||||
selector: '.lm-TabBar-tab',
|
||||
});
|
||||
}
|
||||
const viewManager = ctx.get<ViewManager>(ViewManager);
|
||||
await viewManager.attach(opts);
|
||||
const eventService = ctx.container.get<EventService>(EventService);
|
||||
const menuService = ctx.container.get<MenuService>(MenuService);
|
||||
// 劫持全局的 contextmenu
|
||||
eventService.listenGlobalEvent('contextmenu', (e: React.MouseEvent) => {
|
||||
if (domEditable(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const hasMenu = menuService.open(e);
|
||||
if (!opts.presetConfig?.disableContextMenu || hasMenu) {
|
||||
// 在 ide 内部永远阻塞右键
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
onDispose: ctx => {
|
||||
const layoutRestorer = ctx.get<LayoutRestorer>(LayoutRestorer);
|
||||
layoutRestorer.storeLayout();
|
||||
},
|
||||
});
|
||||
22
frontend/packages/project-ide/view/src/hooks/index.ts
Normal file
22
frontend/packages/project-ide/view/src/hooks/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { useCurrentWidget } from './use-current-widget';
|
||||
export { useCurrentWidgetFromArea } from './use-current-widget-from-area';
|
||||
export {
|
||||
useCurrentResource,
|
||||
CurrentResourceContext,
|
||||
} from './use-current-resource';
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
type Resource,
|
||||
useIDEService,
|
||||
ResourceService,
|
||||
} from '@coze-project-ide/core';
|
||||
|
||||
import { useCurrentWidget } from './use-current-widget';
|
||||
|
||||
export const CurrentResourceContext = React.createContext<Resource | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export function useCurrentResource<T extends Resource>(): T {
|
||||
const currentResource = React.useContext(CurrentResourceContext);
|
||||
if (currentResource) {
|
||||
return currentResource as T;
|
||||
}
|
||||
const resourceService = useIDEService<ResourceService>(ResourceService);
|
||||
const widget = useCurrentWidget();
|
||||
const uri = widget.getResourceURI();
|
||||
if (!uri) {
|
||||
throw new Error('Cannot get uri from widget');
|
||||
}
|
||||
return resourceService.get(uri);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useIDEService, useRefresh } from '@coze-project-ide/core';
|
||||
|
||||
import { type ReactWidget } from '../widget/react-widget';
|
||||
import { type LayoutPanelType } from '../types';
|
||||
import { ApplicationShell } from '../shell/application-shell';
|
||||
export function useCurrentWidgetFromArea<T extends ReactWidget>(
|
||||
area: LayoutPanelType.MAIN_PANEL | LayoutPanelType.BOTTOM_PANEL,
|
||||
): T | undefined {
|
||||
const shell = useIDEService<ApplicationShell>(ApplicationShell);
|
||||
const refresh = useRefresh();
|
||||
useEffect(() => {
|
||||
const dispose = shell.onCurrentWidgetChange(() => {
|
||||
refresh();
|
||||
});
|
||||
return () => dispose.dispose();
|
||||
}, [shell]);
|
||||
return shell.getCurrentWidget(area) as T;
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { type ReactWidget, ReactWidgetContext } from '../widget/react-widget';
|
||||
|
||||
export function useCurrentWidget<T extends ReactWidget>(): T {
|
||||
const widget = useContext<ReactWidget | undefined>(ReactWidgetContext);
|
||||
if (widget?.wrapperWidget) {
|
||||
return widget?.wrapperWidget as T;
|
||||
}
|
||||
if (!widget) {
|
||||
throw new Error(
|
||||
'[useCurrentWidget] Undefined react widget from ide context',
|
||||
);
|
||||
}
|
||||
return widget as T;
|
||||
}
|
||||
162
frontend/packages/project-ide/view/src/index.css
Normal file
162
frontend/packages/project-ide/view/src/index.css
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Container style
|
||||
*/
|
||||
.ide-ps {
|
||||
overflow: hidden !important;
|
||||
overflow-anchor: none;
|
||||
-ms-overflow-style: none;
|
||||
touch-action: auto;
|
||||
-ms-touch-action: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scrollbar rail styles
|
||||
*/
|
||||
.ide-ps__rail-x {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: background-color .2s linear, opacity .2s linear;
|
||||
-webkit-transition: background-color .2s linear, opacity .2s linear;
|
||||
height: 15px;
|
||||
/* there must be 'bottom' or 'top' for ide-ps__rail-x */
|
||||
bottom: 0px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ide-ps__rail-y {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: background-color .2s linear, opacity .2s linear;
|
||||
-webkit-transition: background-color .2s linear, opacity .2s linear;
|
||||
width: 15px;
|
||||
/* there must be 'right' or 'left' for ide-ps__rail-y */
|
||||
right: 0;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ide-ps--active-x > .ide-ps__rail-x,
|
||||
.ide-ps--active-y > .ide-ps__rail-y {
|
||||
display: block;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ide-ps:hover > .ide-ps__rail-x,
|
||||
.ide-ps:hover > .ide-ps__rail-y,
|
||||
.ide-ps--focus > .ide-ps__rail-x,
|
||||
.ide-ps--focus > .ide-ps__rail-y,
|
||||
.ide-ps--scrolling-x > .ide-ps__rail-x,
|
||||
.ide-ps--scrolling-y > .ide-ps__rail-y {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.ide-ps .ide-ps__rail-x:hover,
|
||||
.ide-ps .ide-ps__rail-y:hover,
|
||||
.ide-ps .ide-ps__rail-x:focus,
|
||||
.ide-ps .ide-ps__rail-y:focus,
|
||||
.ide-ps .ide-ps__rail-x.ide-ps--clicking,
|
||||
.ide-ps .ide-ps__rail-y.ide-ps--clicking {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/*
|
||||
* Scrollbar thumb styles
|
||||
*/
|
||||
.ide-ps__thumb-x {
|
||||
background-color: #aaa;
|
||||
border-radius: 6px;
|
||||
transition: background-color .2s linear, height .2s ease-in-out;
|
||||
-webkit-transition: background-color .2s linear, height .2s ease-in-out;
|
||||
height: 6px;
|
||||
/* there must be 'bottom' for ide-ps__thumb-x */
|
||||
bottom: 2px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ide-ps__thumb-y {
|
||||
background-color: #aaa;
|
||||
border-radius: 6px;
|
||||
transition: background-color .2s linear, width .2s ease-in-out;
|
||||
-webkit-transition: background-color .2s linear, width .2s ease-in-out;
|
||||
width: 6px;
|
||||
/* there must be 'right' for ide-ps__thumb-y */
|
||||
right: 2px;
|
||||
/* please don't change 'position' */
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ide-ps__rail-x:hover > .ide-ps__thumb-x,
|
||||
.ide-ps__rail-x:focus > .ide-ps__thumb-x,
|
||||
.ide-ps__rail-x.ide-ps--clicking .ide-ps__thumb-x {
|
||||
background-color: #999;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
.ide-ps__rail-y:hover > .ide-ps__thumb-y,
|
||||
.ide-ps__rail-y:focus > .ide-ps__thumb-y,
|
||||
.ide-ps__rail-y.ide-ps--clicking .ide-ps__thumb-y {
|
||||
background-color: #999;
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
/* MS supports */
|
||||
@supports (-ms-overflow-style: none) .ide-ps {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) .ide-ps {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
|
||||
.lm-Widget::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.lm-Widget::-webkit-scrollbar:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.lm-Widget::-webkit-scrollbar-thumb {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.lm-Widget::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.lm-cursor-backdrop {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin-top: -100px;
|
||||
margin-left: -100px;
|
||||
will-change: transform;
|
||||
z-index: 100;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.lm-cursor-backdrop::after {
|
||||
content: '';
|
||||
height: 1200px;
|
||||
width: 1200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lm-cursor-backdrop::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lm-mod-drag-image {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
will-change: transform;
|
||||
}
|
||||
81
frontend/packages/project-ide/view/src/index.ts
Normal file
81
frontend/packages/project-ide/view/src/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import PerfectScrollbar from './components/scroll-bar';
|
||||
|
||||
export { ReactWidget, ReactWidgetContext } from './widget/react-widget';
|
||||
export {
|
||||
LayoutPanelType,
|
||||
ToolbarAlign,
|
||||
type ViewPluginOptions,
|
||||
type CustomTitleType,
|
||||
type CustomTitleChanged,
|
||||
type PresetConfigType,
|
||||
} from './types';
|
||||
export { ViewManager } from './view-manager';
|
||||
export { WidgetManager } from './widget-manager';
|
||||
export { createViewPlugin } from './create-view-plugin';
|
||||
export { createContextMenuPlugin } from './contributions/context-menu';
|
||||
export {
|
||||
VIEW_CONTAINER_CLASS_NAME,
|
||||
HOVER_TOOLTIP_LABEL,
|
||||
DEBUG_BAR_DRAGGABLE,
|
||||
DISABLE_HANDLE_EVENT,
|
||||
} from './constants';
|
||||
export { WidgetFactory, type ToolbarItem } from './widget/widget-factory';
|
||||
export { HoverService } from './services/hover-service';
|
||||
export { DragService, type DragPropsType } from './services/drag-service';
|
||||
export { MenuService } from './contributions/context-menu';
|
||||
export { ViewService } from './services/view-service';
|
||||
export { DebugService } from './services/debug-service';
|
||||
export { FlowDockPanel } from './widget/dock-panel';
|
||||
import '@vscode/codicons/dist/codicon.css';
|
||||
import './index.css';
|
||||
|
||||
export {
|
||||
ViewContribution,
|
||||
type ViewOptionRegisterService,
|
||||
} from './contributions/view-contribution';
|
||||
export {
|
||||
useCurrentWidget,
|
||||
useCurrentWidgetFromArea,
|
||||
useCurrentResource,
|
||||
CurrentResourceContext,
|
||||
} from './hooks';
|
||||
export {
|
||||
Widget,
|
||||
BoxLayout,
|
||||
SplitLayout,
|
||||
SplitPanel,
|
||||
BoxPanel,
|
||||
DockLayout,
|
||||
TabBar,
|
||||
} from './lumino/widgets';
|
||||
export {
|
||||
StatefulWidget,
|
||||
LayoutRestorer,
|
||||
CustomPreferenceContribution,
|
||||
type CustomPreferenceConfig,
|
||||
} from './shell/layout-restorer';
|
||||
export { ApplicationShell } from './shell/application-shell';
|
||||
|
||||
export { PerfectScrollbar };
|
||||
export { SplitWidget } from './widget/react-widgets/split-widget';
|
||||
export { createBoxLayout, createSplitLayout } from './utils';
|
||||
export { TabBarToolbar } from './widget/tab-bar/toolbar';
|
||||
export { ACTIVITY_BAR_CONTENT } from './widget/react-widgets/activity-bar-widget';
|
||||
export { ViewRenderer } from './view-renderer';
|
||||
export { CustomTabBar } from './widget/tab-bar/custom-tabbar';
|
||||
1506
frontend/packages/project-ide/view/src/lumino/algorithm/array.ts
Normal file
1506
frontend/packages/project-ide/view/src/lumino/algorithm/array.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
254
frontend/packages/project-ide/view/src/lumino/algorithm/find.ts
Normal file
254
frontend/packages/project-ide/view/src/lumino/algorithm/find.ts
Normal 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!];
|
||||
}
|
||||
@@ -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';
|
||||
186
frontend/packages/project-ide/view/src/lumino/algorithm/iter.ts
Normal file
186
frontend/packages/project-ide/view/src/lumino/algorithm/iter.ts
Normal 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;
|
||||
}
|
||||
@@ -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++);
|
||||
}
|
||||
}
|
||||
102
frontend/packages/project-ide/view/src/lumino/algorithm/range.ts
Normal file
102
frontend/packages/project-ide/view/src/lumino/algorithm/range.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
365
frontend/packages/project-ide/view/src/lumino/coreutils/json.ts
Normal file
365
frontend/packages/project-ide/view/src/lumino/coreutils/json.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
114
frontend/packages/project-ide/view/src/lumino/coreutils/mime.ts
Normal file
114
frontend/packages/project-ide/view/src/lumino/coreutils/mime.ts
Normal 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[] = [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
})();
|
||||
}
|
||||
@@ -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;
|
||||
})();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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]]}`;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
1392
frontend/packages/project-ide/view/src/lumino/dragdrop/index.ts
Normal file
1392
frontend/packages/project-ide/view/src/lumino/dragdrop/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
659
frontend/packages/project-ide/view/src/lumino/messaging/index.ts
Normal file
659
frontend/packages/project-ide/view/src/lumino/messaging/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
785
frontend/packages/project-ide/view/src/lumino/signaling/index.ts
Normal file
785
frontend/packages/project-ide/view/src/lumino/signaling/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
1711
frontend/packages/project-ide/view/src/lumino/virtualdom/index.ts
Normal file
1711
frontend/packages/project-ide/view/src/lumino/virtualdom/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
2329
frontend/packages/project-ide/view/src/lumino/widgets/docklayout.ts
Normal file
2329
frontend/packages/project-ide/view/src/lumino/widgets/docklayout.ts
Normal file
File diff suppressed because it is too large
Load Diff
1831
frontend/packages/project-ide/view/src/lumino/widgets/dockpanel.ts
Normal file
1831
frontend/packages/project-ide/view/src/lumino/widgets/dockpanel.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
891
frontend/packages/project-ide/view/src/lumino/widgets/layout.ts
Normal file
891
frontend/packages/project-ide/view/src/lumino/widgets/layout.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
112
frontend/packages/project-ide/view/src/lumino/widgets/panel.ts
Normal file
112
frontend/packages/project-ide/view/src/lumino/widgets/panel.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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[] = [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user