@plastic-software/three 0.181.3 → 0.182.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/build/three.cjs +1192 -522
- package/build/three.core.js +345 -219
- package/build/three.core.min.js +1 -1
- package/build/three.module.js +864 -328
- package/build/three.module.min.js +1 -1
- package/build/three.tsl.js +15 -3
- package/build/three.tsl.min.js +1 -1
- package/build/three.webgpu.js +3660 -1545
- package/build/three.webgpu.min.js +1 -1
- package/build/three.webgpu.nodes.js +3659 -1544
- package/build/three.webgpu.nodes.min.js +1 -1
- package/examples/jsm/controls/MapControls.js +55 -1
- package/examples/jsm/controls/OrbitControls.js +6 -6
- package/examples/jsm/controls/TrackballControls.js +6 -6
- package/examples/jsm/csm/CSM.js +2 -1
- package/examples/jsm/environments/RoomEnvironment.js +2 -0
- package/examples/jsm/geometries/DecalGeometry.js +1 -1
- package/examples/jsm/helpers/LightProbeHelperGPU.js +1 -1
- package/examples/jsm/helpers/TextureHelperGPU.js +1 -1
- package/examples/jsm/inspector/Inspector.js +53 -9
- package/examples/jsm/inspector/RendererInspector.js +12 -2
- package/examples/jsm/inspector/tabs/Console.js +2 -2
- package/examples/jsm/inspector/tabs/Parameters.js +2 -2
- package/examples/jsm/inspector/tabs/Performance.js +2 -2
- package/examples/jsm/inspector/tabs/Viewer.js +4 -4
- package/examples/jsm/inspector/ui/Profiler.js +1836 -31
- package/examples/jsm/inspector/ui/Style.js +948 -13
- package/examples/jsm/inspector/ui/Tab.js +188 -1
- package/examples/jsm/inspector/ui/Values.js +17 -1
- package/examples/jsm/loaders/3DMLoader.js +5 -4
- package/examples/jsm/loaders/DRACOLoader.js +5 -5
- package/examples/jsm/loaders/FBXLoader.js +0 -2
- package/examples/jsm/loaders/HDRLoader.js +0 -1
- package/examples/jsm/loaders/KTX2Loader.js +16 -0
- package/examples/jsm/loaders/LDrawLoader.js +2 -3
- package/examples/jsm/loaders/PCDLoader.js +1 -0
- package/examples/jsm/loaders/SVGLoader.js +1 -1
- package/examples/jsm/loaders/TDSLoader.js +0 -2
- package/examples/jsm/loaders/TGALoader.js +0 -2
- package/examples/jsm/loaders/UltraHDRLoader.js +110 -137
- package/examples/jsm/loaders/VOXLoader.js +660 -117
- package/examples/jsm/loaders/VRMLLoader.js +2 -2
- package/examples/jsm/loaders/usd/USDCParser.js +1 -1
- package/examples/jsm/materials/LDrawConditionalLineNodeMaterial.js +1 -1
- package/examples/jsm/materials/MeshGouraudMaterial.js +0 -1
- package/examples/jsm/materials/WoodNodeMaterial.js +11 -11
- package/examples/jsm/math/Octree.js +131 -1
- package/examples/jsm/misc/Volume.js +0 -1
- package/examples/jsm/misc/VolumeSlice.js +0 -1
- package/examples/jsm/objects/SkyMesh.js +13 -3
- package/examples/jsm/physics/AmmoPhysics.js +12 -7
- package/examples/jsm/physics/JoltPhysics.js +3 -1
- package/examples/jsm/physics/RapierPhysics.js +3 -1
- package/examples/jsm/postprocessing/OutputPass.js +9 -0
- package/examples/jsm/postprocessing/RenderPass.js +10 -0
- package/examples/jsm/postprocessing/UnrealBloomPass.js +48 -18
- package/examples/jsm/renderers/Projector.js +268 -30
- package/examples/jsm/renderers/SVGRenderer.js +191 -58
- package/examples/jsm/shaders/UnpackDepthRGBAShader.js +2 -4
- package/examples/jsm/transpiler/AST.js +44 -0
- package/examples/jsm/transpiler/GLSLDecoder.js +61 -4
- package/examples/jsm/transpiler/ShaderToyDecoder.js +2 -0
- package/examples/jsm/transpiler/TSLEncoder.js +46 -3
- package/examples/jsm/transpiler/TranspilerUtils.js +3 -3
- package/examples/jsm/transpiler/WGSLEncoder.js +27 -0
- package/examples/jsm/tsl/display/AnaglyphPassNode.js +2 -0
- package/examples/jsm/tsl/display/BloomNode.js +11 -1
- package/examples/jsm/tsl/display/GTAONode.js +3 -2
- package/examples/jsm/tsl/display/PixelationPassNode.js +2 -1
- package/examples/jsm/tsl/display/SSGINode.js +7 -19
- package/examples/jsm/tsl/display/SSRNode.js +1 -1
- package/examples/jsm/tsl/display/SSSNode.js +4 -2
- package/examples/jsm/tsl/display/StereoCompositePassNode.js +8 -1
- package/examples/jsm/tsl/display/TRAANode.js +265 -114
- package/examples/jsm/tsl/display/radialBlur.js +68 -0
- package/examples/jsm/utils/ShadowMapViewer.js +24 -10
- package/examples/jsm/utils/ShadowMapViewerGPU.js +1 -1
- package/examples/jsm/utils/WebGPUTextureUtils.js +1 -1
- package/package.json +14 -12
- package/src/Three.Core.js +1 -0
- package/src/Three.TSL.js +14 -2
- package/src/animation/AnimationUtils.js +1 -12
- package/src/animation/KeyframeTrack.js +1 -1
- package/src/animation/tracks/BooleanKeyframeTrack.js +1 -1
- package/src/animation/tracks/ColorKeyframeTrack.js +1 -1
- package/src/animation/tracks/NumberKeyframeTrack.js +1 -1
- package/src/animation/tracks/QuaternionKeyframeTrack.js +1 -1
- package/src/animation/tracks/StringKeyframeTrack.js +1 -1
- package/src/animation/tracks/VectorKeyframeTrack.js +1 -1
- package/src/constants.js +61 -5
- package/src/core/BufferGeometry.js +14 -2
- package/src/core/Raycaster.js +2 -2
- package/src/extras/PMREMGenerator.js +3 -10
- package/src/extras/TextureUtils.js +5 -1
- package/src/geometries/ExtrudeGeometry.js +2 -2
- package/src/geometries/PolyhedronGeometry.js +1 -1
- package/src/helpers/PointLightHelper.js +1 -1
- package/src/lights/DirectionalLight.js +13 -0
- package/src/lights/HemisphereLight.js +10 -0
- package/src/lights/Light.js +1 -11
- package/src/lights/LightProbe.js +0 -15
- package/src/lights/LightShadow.js +0 -3
- package/src/lights/PointLight.js +15 -0
- package/src/lights/PointLightShadow.js +0 -86
- package/src/lights/SpotLight.js +22 -1
- package/src/loaders/MaterialLoader.js +2 -1
- package/src/loaders/ObjectLoader.js +3 -1
- package/src/loaders/nodes/NodeLoader.js +2 -2
- package/src/materials/Material.js +2 -0
- package/src/materials/ShaderMaterial.js +20 -1
- package/src/materials/nodes/Line2NodeMaterial.js +2 -2
- package/src/materials/nodes/MeshPhysicalNodeMaterial.js +3 -2
- package/src/materials/nodes/MeshStandardNodeMaterial.js +5 -4
- package/src/materials/nodes/NodeMaterial.js +59 -3
- package/src/materials/nodes/manager/NodeMaterialObserver.js +1 -1
- package/src/math/Matrix4.js +40 -40
- package/src/math/Sphere.js +1 -1
- package/src/math/Vector3.js +0 -2
- package/src/nodes/TSL.js +4 -1
- package/src/nodes/accessors/BatchNode.js +10 -10
- package/src/nodes/accessors/BufferAttributeNode.js +98 -12
- package/src/nodes/accessors/BufferNode.js +29 -2
- package/src/nodes/accessors/ClippingNode.js +4 -4
- package/src/nodes/accessors/CubeTextureNode.js +20 -1
- package/src/nodes/accessors/InstanceNode.js +69 -29
- package/src/nodes/accessors/MaterialNode.js +9 -1
- package/src/nodes/accessors/MaterialReferenceNode.js +1 -2
- package/src/nodes/accessors/ModelNode.js +1 -1
- package/src/nodes/accessors/Normal.js +2 -2
- package/src/nodes/accessors/ReferenceBaseNode.js +4 -4
- package/src/nodes/accessors/ReferenceNode.js +4 -4
- package/src/nodes/accessors/RendererReferenceNode.js +1 -2
- package/src/nodes/accessors/SkinningNode.js +15 -2
- package/src/nodes/accessors/StorageBufferNode.js +4 -2
- package/src/nodes/accessors/Tangent.js +1 -11
- package/src/nodes/accessors/Texture3DNode.js +26 -1
- package/src/nodes/accessors/UniformArrayNode.js +2 -2
- package/src/nodes/accessors/UserDataNode.js +1 -2
- package/src/nodes/accessors/VertexColorNode.js +1 -2
- package/src/nodes/code/FunctionNode.js +1 -2
- package/src/nodes/core/ArrayNode.js +20 -1
- package/src/nodes/core/AssignNode.js +2 -2
- package/src/nodes/core/AttributeNode.js +2 -2
- package/src/nodes/core/ContextNode.js +103 -4
- package/src/nodes/core/NodeBuilder.js +56 -14
- package/src/nodes/core/NodeFrame.js +12 -4
- package/src/nodes/core/NodeUtils.js +5 -5
- package/src/nodes/core/ParameterNode.js +1 -2
- package/src/nodes/core/PropertyNode.js +19 -3
- package/src/nodes/core/StackNode.js +56 -8
- package/src/nodes/core/StructNode.js +1 -2
- package/src/nodes/core/StructTypeNode.js +11 -17
- package/src/nodes/core/UniformNode.js +19 -4
- package/src/nodes/core/VarNode.js +46 -21
- package/src/nodes/display/NormalMapNode.js +37 -2
- package/src/nodes/display/PassNode.js +77 -7
- package/src/nodes/display/ScreenNode.js +1 -0
- package/src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js +3 -3
- package/src/nodes/functions/BSDF/DFGLUT.js +56 -0
- package/src/nodes/functions/BSDF/EnvironmentBRDF.js +2 -2
- package/src/nodes/functions/BSDF/V_GGX_SmithCorrelated_Anisotropic.js +1 -1
- package/src/nodes/functions/PhysicalLightingModel.js +102 -43
- package/src/nodes/gpgpu/ComputeBuiltinNode.js +1 -2
- package/src/nodes/gpgpu/SubgroupFunctionNode.js +1 -1
- package/src/nodes/gpgpu/WorkgroupInfoNode.js +2 -3
- package/src/nodes/lighting/AnalyticLightNode.js +53 -0
- package/src/nodes/lighting/LightsNode.js +2 -2
- package/src/nodes/lighting/PointShadowNode.js +141 -140
- package/src/nodes/lighting/ShadowFilterNode.js +53 -37
- package/src/nodes/lighting/ShadowNode.js +53 -19
- package/src/nodes/math/BitcountNode.js +433 -0
- package/src/nodes/math/PackFloatNode.js +98 -0
- package/src/nodes/math/UnpackFloatNode.js +96 -0
- package/src/nodes/pmrem/PMREMNode.js +1 -1
- package/src/nodes/tsl/TSLCore.js +4 -4
- package/src/nodes/utils/ArrayElementNode.js +13 -0
- package/src/nodes/utils/EventNode.js +1 -2
- package/src/nodes/utils/Packing.js +13 -1
- package/src/nodes/utils/PostProcessingUtils.js +33 -1
- package/src/nodes/utils/ReflectorNode.js +1 -1
- package/src/nodes/utils/SampleNode.js +1 -1
- package/src/nodes/utils/UVUtils.js +26 -0
- package/src/objects/BatchedMesh.js +5 -2
- package/src/objects/Line.js +1 -1
- package/src/objects/Mesh.js +1 -1
- package/src/objects/Points.js +1 -1
- package/src/objects/Skeleton.js +9 -0
- package/src/renderers/WebGLRenderer.js +145 -33
- package/src/renderers/common/Backend.js +8 -0
- package/src/renderers/common/Background.js +19 -9
- package/src/renderers/common/Binding.js +11 -0
- package/src/renderers/common/Bindings.js +7 -7
- package/src/renderers/common/Buffer.js +40 -0
- package/src/renderers/common/ChainMap.js +30 -6
- package/src/renderers/common/Geometries.js +12 -0
- package/src/renderers/common/RenderContexts.js +8 -1
- package/src/renderers/common/RenderObject.js +14 -1
- package/src/renderers/common/Renderer.js +53 -35
- package/src/renderers/common/Textures.js +1 -1
- package/src/renderers/common/UniformsGroup.js +1 -0
- package/src/renderers/common/XRManager.js +1 -0
- package/src/renderers/common/extras/PMREMGenerator.js +2 -8
- package/src/renderers/common/nodes/NodeUniformBuffer.js +52 -0
- package/src/renderers/shaders/DFGLUTData.js +19 -34
- package/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +5 -2
- package/src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js +8 -4
- package/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js +90 -51
- package/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js +194 -186
- package/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js +1 -1
- package/src/renderers/shaders/ShaderChunk.js +3 -3
- package/src/renderers/shaders/ShaderLib/depth.glsl.js +3 -0
- package/src/renderers/shaders/ShaderLib/{distanceRGBA.glsl.js → distance.glsl.js} +1 -2
- package/src/renderers/shaders/ShaderLib/meshlambert.glsl.js +0 -1
- package/src/renderers/shaders/ShaderLib/meshnormal.glsl.js +1 -2
- package/src/renderers/shaders/ShaderLib/meshphong.glsl.js +0 -1
- package/src/renderers/shaders/ShaderLib/meshphysical.glsl.js +4 -9
- package/src/renderers/shaders/ShaderLib/meshtoon.glsl.js +0 -1
- package/src/renderers/shaders/ShaderLib/shadow.glsl.js +0 -1
- package/src/renderers/shaders/ShaderLib/vsm.glsl.js +4 -6
- package/src/renderers/shaders/ShaderLib.js +3 -3
- package/src/renderers/webgl/WebGLCapabilities.js +3 -4
- package/src/renderers/webgl/WebGLLights.js +18 -1
- package/src/renderers/webgl/WebGLOutput.js +267 -0
- package/src/renderers/webgl/WebGLProgram.js +43 -107
- package/src/renderers/webgl/WebGLPrograms.js +35 -45
- package/src/renderers/webgl/WebGLShadowMap.js +188 -25
- package/src/renderers/webgl/WebGLState.js +20 -20
- package/src/renderers/webgl/WebGLTextures.js +89 -28
- package/src/renderers/webgl/WebGLUniforms.js +40 -3
- package/src/renderers/webgl/WebGLUtils.js +6 -2
- package/src/renderers/webgl-fallback/WebGLBackend.js +79 -13
- package/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +59 -7
- package/src/renderers/webgl-fallback/utils/WebGLState.js +18 -3
- package/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js +5 -3
- package/src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js +9 -9
- package/src/renderers/webgl-fallback/utils/WebGLUtils.js +6 -2
- package/src/renderers/webgpu/WebGPUBackend.js +61 -4
- package/src/renderers/webgpu/WebGPURenderer.js +1 -1
- package/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +65 -23
- package/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +4 -17
- package/src/renderers/webgpu/utils/WebGPUBindingUtils.js +354 -186
- package/src/renderers/webgpu/utils/WebGPUConstants.js +2 -0
- package/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +20 -7
- package/src/renderers/webgpu/utils/WebGPUTextureUtils.js +40 -17
- package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +7 -7
- package/src/renderers/webgpu/utils/WebGPUUtils.js +7 -5
- package/src/textures/CubeDepthTexture.js +76 -0
- package/src/textures/Source.js +1 -1
- package/src/textures/Texture.js +1 -1
- package/src/utils.js +13 -1
- package/src/nodes/functions/BSDF/DFGApprox.js +0 -71
|
@@ -7,13 +7,176 @@ export class Profiler {
|
|
|
7
7
|
this.tabs = {};
|
|
8
8
|
this.activeTabId = null;
|
|
9
9
|
this.isResizing = false;
|
|
10
|
-
this.
|
|
10
|
+
this.lastHeightBottom = 350; // Height for bottom position
|
|
11
|
+
this.lastWidthRight = 450; // Width for right position
|
|
12
|
+
this.position = 'bottom'; // 'bottom' or 'right'
|
|
13
|
+
this.detachedWindows = []; // Array to store detached tab windows
|
|
14
|
+
this.isMobile = this.detectMobile();
|
|
15
|
+
this.maxZIndex = 1002; // Track the highest z-index for detached windows (starts at base z-index from CSS)
|
|
16
|
+
this.nextTabOriginalIndex = 0; // Track the original order of tabs as they are added
|
|
11
17
|
|
|
12
18
|
Style.init();
|
|
13
19
|
|
|
14
20
|
this.setupShell();
|
|
15
21
|
this.setupResizing();
|
|
16
22
|
|
|
23
|
+
// Setup orientation change listener for mobile devices
|
|
24
|
+
if ( this.isMobile ) {
|
|
25
|
+
|
|
26
|
+
this.setupOrientationListener();
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Setup window resize listener to constrain detached windows
|
|
31
|
+
this.setupWindowResizeListener();
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
detectMobile() {
|
|
36
|
+
|
|
37
|
+
// Check for mobile devices
|
|
38
|
+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
39
|
+
const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( userAgent );
|
|
40
|
+
const isTouchDevice = ( 'ontouchstart' in window ) || ( navigator.maxTouchPoints > 0 );
|
|
41
|
+
const isSmallScreen = window.innerWidth <= 768;
|
|
42
|
+
|
|
43
|
+
return isMobileUA || ( isTouchDevice && isSmallScreen );
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setupOrientationListener() {
|
|
48
|
+
|
|
49
|
+
const handleOrientationChange = () => {
|
|
50
|
+
|
|
51
|
+
// Check if device is in landscape or portrait mode
|
|
52
|
+
const isLandscape = window.innerWidth > window.innerHeight;
|
|
53
|
+
|
|
54
|
+
// In landscape mode, use right position (vertical panel)
|
|
55
|
+
// In portrait mode, use bottom position (horizontal panel)
|
|
56
|
+
const targetPosition = isLandscape ? 'right' : 'bottom';
|
|
57
|
+
|
|
58
|
+
if ( this.position !== targetPosition ) {
|
|
59
|
+
|
|
60
|
+
this.setPosition( targetPosition );
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Initial check
|
|
67
|
+
handleOrientationChange();
|
|
68
|
+
|
|
69
|
+
// Listen for orientation changes
|
|
70
|
+
window.addEventListener( 'orientationchange', handleOrientationChange );
|
|
71
|
+
window.addEventListener( 'resize', handleOrientationChange );
|
|
72
|
+
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setupWindowResizeListener() {
|
|
76
|
+
|
|
77
|
+
const constrainDetachedWindows = () => {
|
|
78
|
+
|
|
79
|
+
this.detachedWindows.forEach( detachedWindow => {
|
|
80
|
+
|
|
81
|
+
this.constrainWindowToBounds( detachedWindow.panel );
|
|
82
|
+
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const constrainMainPanel = () => {
|
|
88
|
+
|
|
89
|
+
// Skip if panel is maximized (it should always fill the screen)
|
|
90
|
+
if ( this.panel.classList.contains( 'maximized' ) ) return;
|
|
91
|
+
|
|
92
|
+
const windowWidth = window.innerWidth;
|
|
93
|
+
const windowHeight = window.innerHeight;
|
|
94
|
+
|
|
95
|
+
if ( this.position === 'bottom' ) {
|
|
96
|
+
|
|
97
|
+
const currentHeight = this.panel.offsetHeight;
|
|
98
|
+
const maxHeight = windowHeight - 50; // Leave 50px margin
|
|
99
|
+
|
|
100
|
+
if ( currentHeight > maxHeight ) {
|
|
101
|
+
|
|
102
|
+
this.panel.style.height = `${ maxHeight }px`;
|
|
103
|
+
this.lastHeightBottom = maxHeight;
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
} else if ( this.position === 'right' ) {
|
|
108
|
+
|
|
109
|
+
const currentWidth = this.panel.offsetWidth;
|
|
110
|
+
const maxWidth = windowWidth - 50; // Leave 50px margin
|
|
111
|
+
|
|
112
|
+
if ( currentWidth > maxWidth ) {
|
|
113
|
+
|
|
114
|
+
this.panel.style.width = `${ maxWidth }px`;
|
|
115
|
+
this.lastWidthRight = maxWidth;
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Listen for window resize events
|
|
124
|
+
window.addEventListener( 'resize', () => {
|
|
125
|
+
|
|
126
|
+
constrainDetachedWindows();
|
|
127
|
+
constrainMainPanel();
|
|
128
|
+
|
|
129
|
+
} );
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
constrainWindowToBounds( windowPanel ) {
|
|
134
|
+
|
|
135
|
+
const windowWidth = window.innerWidth;
|
|
136
|
+
const windowHeight = window.innerHeight;
|
|
137
|
+
|
|
138
|
+
const panelWidth = windowPanel.offsetWidth;
|
|
139
|
+
const panelHeight = windowPanel.offsetHeight;
|
|
140
|
+
|
|
141
|
+
let left = parseFloat( windowPanel.style.left ) || windowPanel.offsetLeft || 0;
|
|
142
|
+
let top = parseFloat( windowPanel.style.top ) || windowPanel.offsetTop || 0;
|
|
143
|
+
|
|
144
|
+
// Allow window to extend half its width/height outside the screen
|
|
145
|
+
const halfWidth = panelWidth / 2;
|
|
146
|
+
const halfHeight = panelHeight / 2;
|
|
147
|
+
|
|
148
|
+
// Constrain horizontal position (allow half width to extend beyond right edge)
|
|
149
|
+
if ( left + panelWidth > windowWidth + halfWidth ) {
|
|
150
|
+
|
|
151
|
+
left = windowWidth + halfWidth - panelWidth;
|
|
152
|
+
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Constrain horizontal position (allow half width to extend beyond left edge)
|
|
156
|
+
if ( left < - halfWidth ) {
|
|
157
|
+
|
|
158
|
+
left = - halfWidth;
|
|
159
|
+
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Constrain vertical position (allow half height to extend beyond bottom edge)
|
|
163
|
+
if ( top + panelHeight > windowHeight + halfHeight ) {
|
|
164
|
+
|
|
165
|
+
top = windowHeight + halfHeight - panelHeight;
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Constrain vertical position (allow half height to extend beyond top edge)
|
|
170
|
+
if ( top < - halfHeight ) {
|
|
171
|
+
|
|
172
|
+
top = - halfHeight;
|
|
173
|
+
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Apply constrained position
|
|
177
|
+
windowPanel.style.left = `${ left }px`;
|
|
178
|
+
windowPanel.style.top = `${ top }px`;
|
|
179
|
+
|
|
17
180
|
}
|
|
18
181
|
|
|
19
182
|
setupShell() {
|
|
@@ -24,17 +187,24 @@ export class Profiler {
|
|
|
24
187
|
this.toggleButton = document.createElement( 'button' );
|
|
25
188
|
this.toggleButton.id = 'profiler-toggle';
|
|
26
189
|
this.toggleButton.innerHTML = `
|
|
190
|
+
<span id="builtin-tabs-container"></span>
|
|
27
191
|
<span id="toggle-text">
|
|
28
192
|
<span id="fps-counter">-</span>
|
|
29
193
|
<span class="fps-label">FPS</span>
|
|
30
194
|
</span>
|
|
31
|
-
<!-- <span class="toggle-separator"></span> -->
|
|
32
195
|
<span id="toggle-icon">
|
|
33
196
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-ipad-horizontal-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.5 20h-6.5a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" /><path d="M9 17h2" /><path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M20.2 20.2l1.8 1.8" /></svg>
|
|
34
197
|
</span>
|
|
35
198
|
`;
|
|
36
199
|
this.toggleButton.onclick = () => this.togglePanel();
|
|
37
200
|
|
|
201
|
+
this.builtinTabsContainer = this.toggleButton.querySelector( '#builtin-tabs-container' );
|
|
202
|
+
|
|
203
|
+
// Create mini-panel for builtin tabs (shown when panel is hidden)
|
|
204
|
+
this.miniPanel = document.createElement( 'div' );
|
|
205
|
+
this.miniPanel.id = 'profiler-mini-panel';
|
|
206
|
+
this.miniPanel.className = 'profiler-mini-panel';
|
|
207
|
+
|
|
38
208
|
this.panel = document.createElement( 'div' );
|
|
39
209
|
this.panel.id = 'profiler-panel';
|
|
40
210
|
|
|
@@ -46,6 +216,20 @@ export class Profiler {
|
|
|
46
216
|
const controls = document.createElement( 'div' );
|
|
47
217
|
controls.className = 'profiler-controls';
|
|
48
218
|
|
|
219
|
+
this.floatingBtn = document.createElement( 'button' );
|
|
220
|
+
this.floatingBtn.id = 'floating-btn';
|
|
221
|
+
this.floatingBtn.title = 'Switch to Right Side';
|
|
222
|
+
this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>';
|
|
223
|
+
this.floatingBtn.onclick = () => this.togglePosition();
|
|
224
|
+
|
|
225
|
+
// Hide position toggle button on mobile devices
|
|
226
|
+
if ( this.isMobile ) {
|
|
227
|
+
|
|
228
|
+
this.floatingBtn.style.display = 'none';
|
|
229
|
+
this.panel.classList.add( 'hide-position-toggle' );
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
49
233
|
this.maximizeBtn = document.createElement( 'button' );
|
|
50
234
|
this.maximizeBtn.id = 'maximize-btn';
|
|
51
235
|
this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
|
|
@@ -56,7 +240,7 @@ export class Profiler {
|
|
|
56
240
|
hideBtn.textContent = '-';
|
|
57
241
|
hideBtn.onclick = () => this.togglePanel();
|
|
58
242
|
|
|
59
|
-
controls.append( this.maximizeBtn, hideBtn );
|
|
243
|
+
controls.append( this.floatingBtn, this.maximizeBtn, hideBtn );
|
|
60
244
|
header.append( this.tabsContainer, controls );
|
|
61
245
|
|
|
62
246
|
this.contentWrapper = document.createElement( 'div' );
|
|
@@ -67,7 +251,10 @@ export class Profiler {
|
|
|
67
251
|
|
|
68
252
|
this.panel.append( resizer, header, this.contentWrapper );
|
|
69
253
|
|
|
70
|
-
this.domElement.append( this.toggleButton, this.panel );
|
|
254
|
+
this.domElement.append( this.toggleButton, this.miniPanel, this.panel );
|
|
255
|
+
|
|
256
|
+
// Set initial position class
|
|
257
|
+
this.panel.classList.add( `position-${this.position}` );
|
|
71
258
|
|
|
72
259
|
}
|
|
73
260
|
|
|
@@ -79,18 +266,40 @@ export class Profiler {
|
|
|
79
266
|
|
|
80
267
|
this.isResizing = true;
|
|
81
268
|
this.panel.classList.add( 'resizing' );
|
|
82
|
-
|
|
269
|
+
resizer.setPointerCapture( e.pointerId );
|
|
270
|
+
const startX = e.clientX;
|
|
271
|
+
const startY = e.clientY;
|
|
83
272
|
const startHeight = this.panel.offsetHeight;
|
|
273
|
+
const startWidth = this.panel.offsetWidth;
|
|
84
274
|
|
|
85
275
|
const onMove = ( moveEvent ) => {
|
|
86
276
|
|
|
87
277
|
if ( ! this.isResizing ) return;
|
|
88
278
|
moveEvent.preventDefault();
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
279
|
+
const currentX = moveEvent.clientX;
|
|
280
|
+
const currentY = moveEvent.clientY;
|
|
281
|
+
|
|
282
|
+
if ( this.position === 'bottom' ) {
|
|
283
|
+
|
|
284
|
+
// Vertical resize for bottom position
|
|
285
|
+
const newHeight = startHeight - ( currentY - startY );
|
|
286
|
+
|
|
287
|
+
if ( newHeight > 100 && newHeight < window.innerHeight - 50 ) {
|
|
92
288
|
|
|
93
|
-
|
|
289
|
+
this.panel.style.height = `${ newHeight }px`;
|
|
290
|
+
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
} else if ( this.position === 'right' ) {
|
|
294
|
+
|
|
295
|
+
// Horizontal resize for right position
|
|
296
|
+
const newWidth = startWidth - ( currentX - startX );
|
|
297
|
+
|
|
298
|
+
if ( newWidth > 200 && newWidth < window.innerWidth - 50 ) {
|
|
299
|
+
|
|
300
|
+
this.panel.style.width = `${ newWidth }px`;
|
|
301
|
+
|
|
302
|
+
}
|
|
94
303
|
|
|
95
304
|
}
|
|
96
305
|
|
|
@@ -100,27 +309,36 @@ export class Profiler {
|
|
|
100
309
|
|
|
101
310
|
this.isResizing = false;
|
|
102
311
|
this.panel.classList.remove( 'resizing' );
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
document.removeEventListener( 'touchend', onEnd );
|
|
312
|
+
resizer.removeEventListener( 'pointermove', onMove );
|
|
313
|
+
resizer.removeEventListener( 'pointerup', onEnd );
|
|
314
|
+
resizer.removeEventListener( 'pointercancel', onEnd );
|
|
107
315
|
if ( ! this.panel.classList.contains( 'maximized' ) ) {
|
|
108
316
|
|
|
109
|
-
|
|
317
|
+
// Save dimensions based on current position
|
|
318
|
+
if ( this.position === 'bottom' ) {
|
|
319
|
+
|
|
320
|
+
this.lastHeightBottom = this.panel.offsetHeight;
|
|
321
|
+
|
|
322
|
+
} else if ( this.position === 'right' ) {
|
|
323
|
+
|
|
324
|
+
this.lastWidthRight = this.panel.offsetWidth;
|
|
325
|
+
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Save layout after resize
|
|
329
|
+
this.saveLayout();
|
|
110
330
|
|
|
111
331
|
}
|
|
112
332
|
|
|
113
333
|
};
|
|
114
334
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
document.addEventListener( 'touchend', onEnd );
|
|
335
|
+
resizer.addEventListener( 'pointermove', onMove );
|
|
336
|
+
resizer.addEventListener( 'pointerup', onEnd );
|
|
337
|
+
resizer.addEventListener( 'pointercancel', onEnd );
|
|
119
338
|
|
|
120
339
|
};
|
|
121
340
|
|
|
122
|
-
resizer.addEventListener( '
|
|
123
|
-
resizer.addEventListener( 'touchstart', onStart );
|
|
341
|
+
resizer.addEventListener( 'pointerdown', onStart );
|
|
124
342
|
|
|
125
343
|
}
|
|
126
344
|
|
|
@@ -129,14 +347,50 @@ export class Profiler {
|
|
|
129
347
|
if ( this.panel.classList.contains( 'maximized' ) ) {
|
|
130
348
|
|
|
131
349
|
this.panel.classList.remove( 'maximized' );
|
|
132
|
-
|
|
350
|
+
|
|
351
|
+
// Restore size based on current position
|
|
352
|
+
if ( this.position === 'bottom' ) {
|
|
353
|
+
|
|
354
|
+
this.panel.style.height = `${ this.lastHeightBottom }px`;
|
|
355
|
+
this.panel.style.width = '100%';
|
|
356
|
+
|
|
357
|
+
} else if ( this.position === 'right' ) {
|
|
358
|
+
|
|
359
|
+
this.panel.style.height = '100%';
|
|
360
|
+
this.panel.style.width = `${ this.lastWidthRight }px`;
|
|
361
|
+
|
|
362
|
+
}
|
|
363
|
+
|
|
133
364
|
this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
|
|
134
365
|
|
|
135
366
|
} else {
|
|
136
367
|
|
|
137
|
-
|
|
368
|
+
// Save current size before maximizing
|
|
369
|
+
if ( this.position === 'bottom' ) {
|
|
370
|
+
|
|
371
|
+
this.lastHeightBottom = this.panel.offsetHeight;
|
|
372
|
+
|
|
373
|
+
} else if ( this.position === 'right' ) {
|
|
374
|
+
|
|
375
|
+
this.lastWidthRight = this.panel.offsetWidth;
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
138
379
|
this.panel.classList.add( 'maximized' );
|
|
139
|
-
|
|
380
|
+
|
|
381
|
+
// Maximize based on current position
|
|
382
|
+
if ( this.position === 'bottom' ) {
|
|
383
|
+
|
|
384
|
+
this.panel.style.height = '100vh';
|
|
385
|
+
this.panel.style.width = '100%';
|
|
386
|
+
|
|
387
|
+
} else if ( this.position === 'right' ) {
|
|
388
|
+
|
|
389
|
+
this.panel.style.height = '100%';
|
|
390
|
+
this.panel.style.width = '100vw';
|
|
391
|
+
|
|
392
|
+
}
|
|
393
|
+
|
|
140
394
|
this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>';
|
|
141
395
|
|
|
142
396
|
}
|
|
@@ -146,24 +400,1575 @@ export class Profiler {
|
|
|
146
400
|
addTab( tab ) {
|
|
147
401
|
|
|
148
402
|
this.tabs[ tab.id ] = tab;
|
|
149
|
-
|
|
403
|
+
|
|
404
|
+
// Assign a permanent original index to this tab
|
|
405
|
+
tab.originalIndex = this.nextTabOriginalIndex ++;
|
|
406
|
+
|
|
407
|
+
// Add visual indicator for tabs that cannot be detached
|
|
408
|
+
if ( tab.allowDetach === false ) {
|
|
409
|
+
|
|
410
|
+
tab.button.classList.add( 'no-detach' );
|
|
411
|
+
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Set visibility change callback
|
|
415
|
+
tab.onVisibilityChange = () => this.updatePanelSize();
|
|
416
|
+
|
|
417
|
+
this.setupTabDragAndDrop( tab );
|
|
418
|
+
|
|
150
419
|
this.tabsContainer.appendChild( tab.button );
|
|
151
420
|
this.contentWrapper.appendChild( tab.content );
|
|
152
421
|
|
|
422
|
+
// Apply the current visibility state to the DOM elements
|
|
423
|
+
if ( ! tab.isVisible ) {
|
|
424
|
+
|
|
425
|
+
tab.button.style.display = 'none';
|
|
426
|
+
tab.content.style.display = 'none';
|
|
427
|
+
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// If tab is builtin, add it to the profiler-toggle button
|
|
431
|
+
if ( tab.builtin ) {
|
|
432
|
+
|
|
433
|
+
this.addBuiltinTab( tab );
|
|
434
|
+
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Update panel size when tabs change
|
|
438
|
+
this.updatePanelSize();
|
|
439
|
+
|
|
153
440
|
}
|
|
154
441
|
|
|
155
|
-
|
|
442
|
+
addBuiltinTab( tab ) {
|
|
156
443
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
444
|
+
// Create a button for the builtin tab in the profiler-toggle
|
|
445
|
+
const builtinButton = document.createElement( 'button' );
|
|
446
|
+
builtinButton.className = 'builtin-tab-btn';
|
|
447
|
+
|
|
448
|
+
// Use icon if provided, otherwise use first letter
|
|
449
|
+
if ( tab.icon ) {
|
|
450
|
+
|
|
451
|
+
builtinButton.innerHTML = tab.icon;
|
|
452
|
+
|
|
453
|
+
} else {
|
|
454
|
+
|
|
455
|
+
builtinButton.textContent = tab.button.textContent.charAt( 0 ).toUpperCase();
|
|
456
|
+
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
builtinButton.title = tab.button.textContent;
|
|
460
|
+
|
|
461
|
+
// Create mini-panel content container for this tab
|
|
462
|
+
const miniContent = document.createElement( 'div' );
|
|
463
|
+
miniContent.className = 'mini-panel-content';
|
|
464
|
+
miniContent.style.display = 'none';
|
|
465
|
+
|
|
466
|
+
// Store references in the tab object
|
|
467
|
+
tab.builtinButton = builtinButton;
|
|
468
|
+
tab.miniContent = miniContent;
|
|
469
|
+
|
|
470
|
+
this.miniPanel.appendChild( miniContent );
|
|
471
|
+
|
|
472
|
+
builtinButton.onclick = ( e ) => {
|
|
473
|
+
|
|
474
|
+
e.stopPropagation(); // Prevent toggle panel from triggering
|
|
475
|
+
|
|
476
|
+
const isPanelVisible = this.panel.classList.contains( 'visible' );
|
|
477
|
+
|
|
478
|
+
if ( isPanelVisible ) {
|
|
479
|
+
|
|
480
|
+
// Panel is visible - navigate to tab
|
|
481
|
+
if ( ! tab.isVisible ) {
|
|
482
|
+
|
|
483
|
+
tab.show();
|
|
484
|
+
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if ( tab.isDetached ) {
|
|
488
|
+
|
|
489
|
+
// If tab is detached, just bring its window to front
|
|
490
|
+
if ( tab.detachedWindow ) {
|
|
491
|
+
|
|
492
|
+
this.bringWindowToFront( tab.detachedWindow.panel );
|
|
493
|
+
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
} else {
|
|
497
|
+
|
|
498
|
+
// Activate the tab
|
|
499
|
+
this.setActiveTab( tab.id );
|
|
500
|
+
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
} else {
|
|
504
|
+
|
|
505
|
+
// Panel is hidden - toggle mini-panel for this tab
|
|
506
|
+
const isCurrentlyActive = miniContent.style.display !== 'none' && miniContent.children.length > 0;
|
|
507
|
+
|
|
508
|
+
// Hide all other mini-panel contents
|
|
509
|
+
this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
|
|
510
|
+
|
|
511
|
+
content.style.display = 'none';
|
|
512
|
+
|
|
513
|
+
} );
|
|
514
|
+
|
|
515
|
+
// Remove active state from all builtin buttons
|
|
516
|
+
this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
|
|
517
|
+
|
|
518
|
+
btn.classList.remove( 'active' );
|
|
519
|
+
|
|
520
|
+
} );
|
|
521
|
+
|
|
522
|
+
if ( isCurrentlyActive ) {
|
|
523
|
+
|
|
524
|
+
// Toggle off - hide mini-panel and move content back
|
|
525
|
+
this.miniPanel.classList.remove( 'visible' );
|
|
526
|
+
miniContent.style.display = 'none';
|
|
527
|
+
|
|
528
|
+
// Move content back to main panel
|
|
529
|
+
if ( miniContent.firstChild ) {
|
|
530
|
+
|
|
531
|
+
tab.content.appendChild( miniContent.firstChild );
|
|
532
|
+
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
} else {
|
|
536
|
+
|
|
537
|
+
// Toggle on - show mini-panel with this tab's content
|
|
538
|
+
builtinButton.classList.add( 'active' );
|
|
539
|
+
|
|
540
|
+
// Move actual content to mini-panel (not clone) if not already there
|
|
541
|
+
if ( ! miniContent.firstChild ) {
|
|
542
|
+
|
|
543
|
+
const actualContent = tab.content.querySelector( '.list-scroll-wrapper' ) || tab.content.firstElementChild;
|
|
544
|
+
|
|
545
|
+
if ( actualContent ) {
|
|
546
|
+
|
|
547
|
+
miniContent.appendChild( actualContent );
|
|
548
|
+
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Show after content is moved
|
|
554
|
+
miniContent.style.display = 'block';
|
|
555
|
+
this.miniPanel.classList.add( 'visible' );
|
|
556
|
+
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
this.builtinTabsContainer.appendChild( builtinButton );
|
|
564
|
+
|
|
565
|
+
// Store references
|
|
566
|
+
tab.builtinButton = builtinButton;
|
|
567
|
+
tab.miniContent = miniContent;
|
|
568
|
+
tab.profiler = this;
|
|
569
|
+
|
|
570
|
+
// If the tab was hidden before being added, hide the builtin button
|
|
571
|
+
if ( ! tab.isVisible ) {
|
|
572
|
+
|
|
573
|
+
builtinButton.style.display = 'none';
|
|
574
|
+
miniContent.style.display = 'none';
|
|
575
|
+
|
|
576
|
+
// Hide the builtin-tabs-container if all builtin buttons are hidden
|
|
577
|
+
const hasVisibleBuiltinButtons = Array.from( this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) )
|
|
578
|
+
.some( btn => btn.style.display !== 'none' );
|
|
579
|
+
|
|
580
|
+
if ( ! hasVisibleBuiltinButtons ) {
|
|
581
|
+
|
|
582
|
+
this.builtinTabsContainer.style.display = 'none';
|
|
583
|
+
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
}
|
|
160
587
|
|
|
161
588
|
}
|
|
162
589
|
|
|
163
|
-
|
|
590
|
+
updatePanelSize() {
|
|
164
591
|
|
|
165
|
-
|
|
166
|
-
this.
|
|
592
|
+
// Check if there are any visible tabs in the panel
|
|
593
|
+
const hasVisibleTabs = Object.values( this.tabs ).some( tab => ! tab.isDetached && tab.isVisible );
|
|
594
|
+
|
|
595
|
+
// Add or remove CSS class to indicate no tabs state
|
|
596
|
+
if ( ! hasVisibleTabs ) {
|
|
597
|
+
|
|
598
|
+
this.panel.classList.add( 'no-tabs' );
|
|
599
|
+
|
|
600
|
+
// If maximized and no tabs, restore to normal size
|
|
601
|
+
if ( this.panel.classList.contains( 'maximized' ) ) {
|
|
602
|
+
|
|
603
|
+
this.panel.classList.remove( 'maximized' );
|
|
604
|
+
this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
|
|
605
|
+
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// No tabs visible - set to minimum size
|
|
609
|
+
if ( this.position === 'bottom' ) {
|
|
610
|
+
|
|
611
|
+
this.panel.style.height = '38px';
|
|
612
|
+
|
|
613
|
+
} else if ( this.position === 'right' ) {
|
|
614
|
+
|
|
615
|
+
// 45px = width of one button column
|
|
616
|
+
this.panel.style.width = '45px';
|
|
617
|
+
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
} else {
|
|
621
|
+
|
|
622
|
+
this.panel.classList.remove( 'no-tabs' );
|
|
623
|
+
|
|
624
|
+
if ( Object.keys( this.tabs ).length > 0 ) {
|
|
625
|
+
|
|
626
|
+
// Has tabs - restore to saved size only if we had set it to minimum before
|
|
627
|
+
if ( this.position === 'bottom' ) {
|
|
628
|
+
|
|
629
|
+
const currentHeight = parseInt( this.panel.style.height );
|
|
630
|
+
if ( currentHeight === 38 ) {
|
|
631
|
+
|
|
632
|
+
this.panel.style.height = `${ this.lastHeightBottom }px`;
|
|
633
|
+
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
} else if ( this.position === 'right' ) {
|
|
637
|
+
|
|
638
|
+
const currentWidth = parseInt( this.panel.style.width );
|
|
639
|
+
if ( currentWidth === 45 ) {
|
|
640
|
+
|
|
641
|
+
this.panel.style.width = `${ this.lastWidthRight }px`;
|
|
642
|
+
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
setupTabDragAndDrop( tab ) {
|
|
654
|
+
|
|
655
|
+
// Disable drag and drop on mobile devices
|
|
656
|
+
if ( this.isMobile ) {
|
|
657
|
+
|
|
658
|
+
tab.button.addEventListener( 'click', () => {
|
|
659
|
+
|
|
660
|
+
this.setActiveTab( tab.id );
|
|
661
|
+
|
|
662
|
+
} );
|
|
663
|
+
|
|
664
|
+
return;
|
|
665
|
+
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Disable drag and drop if tab doesn't allow detach
|
|
669
|
+
if ( tab.allowDetach === false ) {
|
|
670
|
+
|
|
671
|
+
tab.button.addEventListener( 'click', () => {
|
|
672
|
+
|
|
673
|
+
this.setActiveTab( tab.id );
|
|
674
|
+
|
|
675
|
+
} );
|
|
676
|
+
|
|
677
|
+
tab.button.style.cursor = 'default';
|
|
678
|
+
|
|
679
|
+
return;
|
|
680
|
+
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let isDragging = false;
|
|
684
|
+
let startX, startY;
|
|
685
|
+
let hasMoved = false;
|
|
686
|
+
let previewWindow = null;
|
|
687
|
+
const dragThreshold = 10; // pixels to move before starting drag
|
|
688
|
+
|
|
689
|
+
const onDragStart = ( e ) => {
|
|
690
|
+
|
|
691
|
+
startX = e.clientX;
|
|
692
|
+
startY = e.clientY;
|
|
693
|
+
isDragging = false;
|
|
694
|
+
hasMoved = false;
|
|
695
|
+
tab.button.setPointerCapture( e.pointerId );
|
|
696
|
+
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const onDragMove = ( e ) => {
|
|
700
|
+
|
|
701
|
+
const currentX = e.clientX;
|
|
702
|
+
const currentY = e.clientY;
|
|
703
|
+
|
|
704
|
+
const deltaX = Math.abs( currentX - startX );
|
|
705
|
+
const deltaY = Math.abs( currentY - startY );
|
|
706
|
+
|
|
707
|
+
if ( ! isDragging && ( deltaX > dragThreshold || deltaY > dragThreshold ) ) {
|
|
708
|
+
|
|
709
|
+
isDragging = true;
|
|
710
|
+
tab.button.style.cursor = 'grabbing';
|
|
711
|
+
tab.button.style.opacity = '0.5';
|
|
712
|
+
tab.button.style.transform = 'scale(1.05)';
|
|
713
|
+
|
|
714
|
+
previewWindow = this.createPreviewWindow( tab, currentX, currentY );
|
|
715
|
+
previewWindow.style.opacity = '0.8';
|
|
716
|
+
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if ( isDragging && previewWindow ) {
|
|
720
|
+
|
|
721
|
+
hasMoved = true;
|
|
722
|
+
e.preventDefault();
|
|
723
|
+
|
|
724
|
+
previewWindow.style.left = `${ currentX - 200 }px`;
|
|
725
|
+
previewWindow.style.top = `${ currentY - 20 }px`;
|
|
726
|
+
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const onDragEnd = () => {
|
|
732
|
+
|
|
733
|
+
if ( isDragging && hasMoved && previewWindow ) {
|
|
734
|
+
|
|
735
|
+
if ( previewWindow.parentNode ) {
|
|
736
|
+
|
|
737
|
+
previewWindow.parentNode.removeChild( previewWindow );
|
|
738
|
+
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const finalX = parseInt( previewWindow.style.left ) + 200;
|
|
742
|
+
const finalY = parseInt( previewWindow.style.top ) + 20;
|
|
743
|
+
|
|
744
|
+
this.detachTab( tab, finalX, finalY );
|
|
745
|
+
|
|
746
|
+
} else if ( ! hasMoved ) {
|
|
747
|
+
|
|
748
|
+
this.setActiveTab( tab.id );
|
|
749
|
+
|
|
750
|
+
if ( previewWindow && previewWindow.parentNode ) {
|
|
751
|
+
|
|
752
|
+
previewWindow.parentNode.removeChild( previewWindow );
|
|
753
|
+
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
} else if ( previewWindow ) {
|
|
757
|
+
|
|
758
|
+
if ( previewWindow.parentNode ) {
|
|
759
|
+
|
|
760
|
+
previewWindow.parentNode.removeChild( previewWindow );
|
|
761
|
+
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
tab.button.style.opacity = '';
|
|
767
|
+
tab.button.style.transform = '';
|
|
768
|
+
tab.button.style.cursor = '';
|
|
769
|
+
isDragging = false;
|
|
770
|
+
hasMoved = false;
|
|
771
|
+
previewWindow = null;
|
|
772
|
+
|
|
773
|
+
tab.button.removeEventListener( 'pointermove', onDragMove );
|
|
774
|
+
tab.button.removeEventListener( 'pointerup', onDragEnd );
|
|
775
|
+
tab.button.removeEventListener( 'pointercancel', onDragEnd );
|
|
776
|
+
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
tab.button.addEventListener( 'pointerdown', ( e ) => {
|
|
780
|
+
|
|
781
|
+
onDragStart( e );
|
|
782
|
+
tab.button.addEventListener( 'pointermove', onDragMove );
|
|
783
|
+
tab.button.addEventListener( 'pointerup', onDragEnd );
|
|
784
|
+
tab.button.addEventListener( 'pointercancel', onDragEnd );
|
|
785
|
+
|
|
786
|
+
} );
|
|
787
|
+
|
|
788
|
+
// Set cursor to grab for tabs that can be detached
|
|
789
|
+
tab.button.style.cursor = 'grab';
|
|
790
|
+
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
createPreviewWindow( tab, x, y ) {
|
|
794
|
+
|
|
795
|
+
const windowPanel = document.createElement( 'div' );
|
|
796
|
+
windowPanel.className = 'detached-tab-panel';
|
|
797
|
+
windowPanel.style.left = `${ x - 200 }px`;
|
|
798
|
+
windowPanel.style.top = `${ y - 20 }px`;
|
|
799
|
+
windowPanel.style.pointerEvents = 'none'; // Preview only
|
|
800
|
+
|
|
801
|
+
// Set z-index for preview window to be on top
|
|
802
|
+
this.maxZIndex ++;
|
|
803
|
+
windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
|
|
804
|
+
|
|
805
|
+
const windowHeader = document.createElement( 'div' );
|
|
806
|
+
windowHeader.className = 'detached-tab-header';
|
|
807
|
+
|
|
808
|
+
const title = document.createElement( 'span' );
|
|
809
|
+
title.textContent = tab.button.textContent.replace( '⇱', '' ).trim();
|
|
810
|
+
windowHeader.appendChild( title );
|
|
811
|
+
|
|
812
|
+
const headerControls = document.createElement( 'div' );
|
|
813
|
+
headerControls.className = 'detached-header-controls';
|
|
814
|
+
|
|
815
|
+
const reattachBtn = document.createElement( 'button' );
|
|
816
|
+
reattachBtn.className = 'detached-reattach-btn';
|
|
817
|
+
reattachBtn.innerHTML = '↩';
|
|
818
|
+
headerControls.appendChild( reattachBtn );
|
|
819
|
+
windowHeader.appendChild( headerControls );
|
|
820
|
+
|
|
821
|
+
const windowContent = document.createElement( 'div' );
|
|
822
|
+
windowContent.className = 'detached-tab-content';
|
|
823
|
+
|
|
824
|
+
const resizer = document.createElement( 'div' );
|
|
825
|
+
resizer.className = 'detached-tab-resizer';
|
|
826
|
+
|
|
827
|
+
windowPanel.appendChild( resizer );
|
|
828
|
+
windowPanel.appendChild( windowHeader );
|
|
829
|
+
windowPanel.appendChild( windowContent );
|
|
830
|
+
|
|
831
|
+
document.body.appendChild( windowPanel );
|
|
832
|
+
|
|
833
|
+
return windowPanel;
|
|
834
|
+
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
detachTab( tab, x, y ) {
|
|
838
|
+
|
|
839
|
+
if ( tab.isDetached ) return;
|
|
840
|
+
|
|
841
|
+
// Check if tab allows detachment
|
|
842
|
+
if ( tab.allowDetach === false ) return;
|
|
843
|
+
|
|
844
|
+
const allButtons = Array.from( this.tabsContainer.children );
|
|
845
|
+
|
|
846
|
+
const tabIdsInOrder = allButtons.map( btn => {
|
|
847
|
+
|
|
848
|
+
return Object.keys( this.tabs ).find( id => this.tabs[ id ].button === btn );
|
|
849
|
+
|
|
850
|
+
} ).filter( id => id !== undefined );
|
|
851
|
+
|
|
852
|
+
const currentIndex = tabIdsInOrder.indexOf( tab.id );
|
|
853
|
+
|
|
854
|
+
let newActiveTab = null;
|
|
855
|
+
|
|
856
|
+
if ( this.activeTabId === tab.id ) {
|
|
857
|
+
|
|
858
|
+
tab.setActive( false );
|
|
859
|
+
|
|
860
|
+
const remainingTabs = tabIdsInOrder.filter( id =>
|
|
861
|
+
id !== tab.id &&
|
|
862
|
+
! this.tabs[ id ].isDetached &&
|
|
863
|
+
this.tabs[ id ].isVisible
|
|
864
|
+
);
|
|
865
|
+
|
|
866
|
+
if ( remainingTabs.length > 0 ) {
|
|
867
|
+
|
|
868
|
+
for ( let i = currentIndex - 1; i >= 0; i -- ) {
|
|
869
|
+
|
|
870
|
+
if ( remainingTabs.includes( tabIdsInOrder[ i ] ) ) {
|
|
871
|
+
|
|
872
|
+
newActiveTab = tabIdsInOrder[ i ];
|
|
873
|
+
break;
|
|
874
|
+
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if ( ! newActiveTab ) {
|
|
880
|
+
|
|
881
|
+
for ( let i = currentIndex + 1; i < tabIdsInOrder.length; i ++ ) {
|
|
882
|
+
|
|
883
|
+
if ( remainingTabs.includes( tabIdsInOrder[ i ] ) ) {
|
|
884
|
+
|
|
885
|
+
newActiveTab = tabIdsInOrder[ i ];
|
|
886
|
+
break;
|
|
887
|
+
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if ( ! newActiveTab ) {
|
|
895
|
+
|
|
896
|
+
newActiveTab = remainingTabs[ 0 ];
|
|
897
|
+
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if ( tab.button.parentNode ) {
|
|
905
|
+
|
|
906
|
+
tab.button.parentNode.removeChild( tab.button );
|
|
907
|
+
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if ( tab.content.parentNode ) {
|
|
911
|
+
|
|
912
|
+
tab.content.parentNode.removeChild( tab.content );
|
|
913
|
+
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const detachedWindow = this.createDetachedWindow( tab, x, y );
|
|
917
|
+
this.detachedWindows.push( detachedWindow );
|
|
918
|
+
|
|
919
|
+
tab.isDetached = true;
|
|
920
|
+
tab.detachedWindow = detachedWindow;
|
|
921
|
+
|
|
922
|
+
if ( newActiveTab ) {
|
|
923
|
+
|
|
924
|
+
this.setActiveTab( newActiveTab );
|
|
925
|
+
|
|
926
|
+
} else if ( this.activeTabId === tab.id ) {
|
|
927
|
+
|
|
928
|
+
this.activeTabId = null;
|
|
929
|
+
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Update panel size after detaching
|
|
933
|
+
this.updatePanelSize();
|
|
934
|
+
|
|
935
|
+
this.saveLayout();
|
|
936
|
+
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
createDetachedWindow( tab, x, y ) {
|
|
940
|
+
|
|
941
|
+
// Constrain initial position to window bounds
|
|
942
|
+
const windowWidth = window.innerWidth;
|
|
943
|
+
const windowHeight = window.innerHeight;
|
|
944
|
+
const estimatedWidth = 400; // Default detached window width
|
|
945
|
+
const estimatedHeight = 300; // Default detached window height
|
|
946
|
+
|
|
947
|
+
let constrainedX = x - 200;
|
|
948
|
+
let constrainedY = y - 20;
|
|
949
|
+
|
|
950
|
+
if ( constrainedX + estimatedWidth > windowWidth ) {
|
|
951
|
+
|
|
952
|
+
constrainedX = windowWidth - estimatedWidth;
|
|
953
|
+
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if ( constrainedX < 0 ) {
|
|
957
|
+
|
|
958
|
+
constrainedX = 0;
|
|
959
|
+
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if ( constrainedY + estimatedHeight > windowHeight ) {
|
|
963
|
+
|
|
964
|
+
constrainedY = windowHeight - estimatedHeight;
|
|
965
|
+
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if ( constrainedY < 0 ) {
|
|
969
|
+
|
|
970
|
+
constrainedY = 0;
|
|
971
|
+
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const windowPanel = document.createElement( 'div' );
|
|
975
|
+
windowPanel.className = 'detached-tab-panel';
|
|
976
|
+
windowPanel.style.left = `${ constrainedX }px`;
|
|
977
|
+
windowPanel.style.top = `${ constrainedY }px`;
|
|
978
|
+
|
|
979
|
+
if ( ! this.panel.classList.contains( 'visible' ) ) {
|
|
980
|
+
|
|
981
|
+
windowPanel.style.opacity = '0';
|
|
982
|
+
windowPanel.style.visibility = 'hidden';
|
|
983
|
+
windowPanel.style.pointerEvents = 'none';
|
|
984
|
+
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Hide detached window if tab is not visible
|
|
988
|
+
if ( ! tab.isVisible ) {
|
|
989
|
+
|
|
990
|
+
windowPanel.style.display = 'none';
|
|
991
|
+
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const windowHeader = document.createElement( 'div' );
|
|
995
|
+
windowHeader.className = 'detached-tab-header';
|
|
996
|
+
|
|
997
|
+
const title = document.createElement( 'span' );
|
|
998
|
+
title.textContent = tab.button.textContent.replace( '⇱', '' ).trim();
|
|
999
|
+
windowHeader.appendChild( title );
|
|
1000
|
+
|
|
1001
|
+
const headerControls = document.createElement( 'div' );
|
|
1002
|
+
headerControls.className = 'detached-header-controls';
|
|
1003
|
+
|
|
1004
|
+
const reattachBtn = document.createElement( 'button' );
|
|
1005
|
+
reattachBtn.className = 'detached-reattach-btn';
|
|
1006
|
+
reattachBtn.innerHTML = '↩';
|
|
1007
|
+
reattachBtn.title = 'Reattach to main panel';
|
|
1008
|
+
reattachBtn.onclick = () => this.reattachTab( tab );
|
|
1009
|
+
|
|
1010
|
+
headerControls.appendChild( reattachBtn );
|
|
1011
|
+
windowHeader.appendChild( headerControls );
|
|
1012
|
+
|
|
1013
|
+
const windowContent = document.createElement( 'div' );
|
|
1014
|
+
windowContent.className = 'detached-tab-content';
|
|
1015
|
+
windowContent.appendChild( tab.content );
|
|
1016
|
+
|
|
1017
|
+
// Make sure content is visible
|
|
1018
|
+
tab.content.style.display = 'block';
|
|
1019
|
+
tab.content.classList.add( 'active' );
|
|
1020
|
+
|
|
1021
|
+
// Create resize handles for all edges
|
|
1022
|
+
const resizerTop = document.createElement( 'div' );
|
|
1023
|
+
resizerTop.className = 'detached-tab-resizer-top';
|
|
1024
|
+
|
|
1025
|
+
const resizerRight = document.createElement( 'div' );
|
|
1026
|
+
resizerRight.className = 'detached-tab-resizer-right';
|
|
1027
|
+
|
|
1028
|
+
const resizerBottom = document.createElement( 'div' );
|
|
1029
|
+
resizerBottom.className = 'detached-tab-resizer-bottom';
|
|
1030
|
+
|
|
1031
|
+
const resizerLeft = document.createElement( 'div' );
|
|
1032
|
+
resizerLeft.className = 'detached-tab-resizer-left';
|
|
1033
|
+
|
|
1034
|
+
const resizerCorner = document.createElement( 'div' );
|
|
1035
|
+
resizerCorner.className = 'detached-tab-resizer';
|
|
1036
|
+
|
|
1037
|
+
windowPanel.appendChild( resizerTop );
|
|
1038
|
+
windowPanel.appendChild( resizerRight );
|
|
1039
|
+
windowPanel.appendChild( resizerBottom );
|
|
1040
|
+
windowPanel.appendChild( resizerLeft );
|
|
1041
|
+
windowPanel.appendChild( resizerCorner );
|
|
1042
|
+
windowPanel.appendChild( windowHeader );
|
|
1043
|
+
windowPanel.appendChild( windowContent );
|
|
1044
|
+
|
|
1045
|
+
document.body.appendChild( windowPanel );
|
|
1046
|
+
|
|
1047
|
+
// Setup window dragging
|
|
1048
|
+
this.setupDetachedWindowDrag( windowPanel, windowHeader, tab );
|
|
1049
|
+
|
|
1050
|
+
// Setup window resizing
|
|
1051
|
+
this.setupDetachedWindowResize( windowPanel, resizerTop, resizerRight, resizerBottom, resizerLeft, resizerCorner );
|
|
1052
|
+
|
|
1053
|
+
// Use the same z-index that was set on the preview window
|
|
1054
|
+
windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
|
|
1055
|
+
|
|
1056
|
+
return { panel: windowPanel, tab: tab };
|
|
1057
|
+
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
bringWindowToFront( windowPanel ) {
|
|
1061
|
+
|
|
1062
|
+
// Increment the max z-index and apply it to the clicked window
|
|
1063
|
+
this.maxZIndex ++;
|
|
1064
|
+
windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
|
|
1065
|
+
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
setupDetachedWindowDrag( windowPanel, header, tab ) {
|
|
1069
|
+
|
|
1070
|
+
let isDragging = false;
|
|
1071
|
+
let startX, startY, startLeft, startTop;
|
|
1072
|
+
|
|
1073
|
+
// Bring window to front when clicking anywhere on it
|
|
1074
|
+
windowPanel.addEventListener( 'pointerdown', () => {
|
|
1075
|
+
|
|
1076
|
+
this.bringWindowToFront( windowPanel );
|
|
1077
|
+
|
|
1078
|
+
} );
|
|
1079
|
+
|
|
1080
|
+
const onDragStart = ( e ) => {
|
|
1081
|
+
|
|
1082
|
+
if ( e.target.classList.contains( 'detached-reattach-btn' ) ) {
|
|
1083
|
+
|
|
1084
|
+
return;
|
|
1085
|
+
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Bring window to front when starting to drag
|
|
1089
|
+
this.bringWindowToFront( windowPanel );
|
|
1090
|
+
|
|
1091
|
+
isDragging = true;
|
|
1092
|
+
header.style.cursor = 'grabbing';
|
|
1093
|
+
header.setPointerCapture( e.pointerId );
|
|
1094
|
+
|
|
1095
|
+
startX = e.clientX;
|
|
1096
|
+
startY = e.clientY;
|
|
1097
|
+
|
|
1098
|
+
const rect = windowPanel.getBoundingClientRect();
|
|
1099
|
+
startLeft = rect.left;
|
|
1100
|
+
startTop = rect.top;
|
|
1101
|
+
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
const onDragMove = ( e ) => {
|
|
1105
|
+
|
|
1106
|
+
if ( ! isDragging ) return;
|
|
1107
|
+
|
|
1108
|
+
e.preventDefault();
|
|
1109
|
+
|
|
1110
|
+
const currentX = e.clientX;
|
|
1111
|
+
const currentY = e.clientY;
|
|
1112
|
+
|
|
1113
|
+
const deltaX = currentX - startX;
|
|
1114
|
+
const deltaY = currentY - startY;
|
|
1115
|
+
|
|
1116
|
+
let newLeft = startLeft + deltaX;
|
|
1117
|
+
let newTop = startTop + deltaY;
|
|
1118
|
+
|
|
1119
|
+
// Constrain to window bounds (allow half width/height to extend outside)
|
|
1120
|
+
const windowWidth = window.innerWidth;
|
|
1121
|
+
const windowHeight = window.innerHeight;
|
|
1122
|
+
const panelWidth = windowPanel.offsetWidth;
|
|
1123
|
+
const panelHeight = windowPanel.offsetHeight;
|
|
1124
|
+
const halfWidth = panelWidth / 2;
|
|
1125
|
+
const halfHeight = panelHeight / 2;
|
|
1126
|
+
|
|
1127
|
+
// Allow window to extend half its width beyond right edge
|
|
1128
|
+
if ( newLeft + panelWidth > windowWidth + halfWidth ) {
|
|
1129
|
+
|
|
1130
|
+
newLeft = windowWidth + halfWidth - panelWidth;
|
|
1131
|
+
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Allow window to extend half its width beyond left edge
|
|
1135
|
+
if ( newLeft < - halfWidth ) {
|
|
1136
|
+
|
|
1137
|
+
newLeft = - halfWidth;
|
|
1138
|
+
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Allow window to extend half its height beyond bottom edge
|
|
1142
|
+
if ( newTop + panelHeight > windowHeight + halfHeight ) {
|
|
1143
|
+
|
|
1144
|
+
newTop = windowHeight + halfHeight - panelHeight;
|
|
1145
|
+
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Allow window to extend half its height beyond top edge
|
|
1149
|
+
if ( newTop < - halfHeight ) {
|
|
1150
|
+
|
|
1151
|
+
newTop = - halfHeight;
|
|
1152
|
+
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
windowPanel.style.left = `${ newLeft }px`;
|
|
1156
|
+
windowPanel.style.top = `${ newTop }px`;
|
|
1157
|
+
|
|
1158
|
+
// Check if cursor is over the inspector panel
|
|
1159
|
+
const panelRect = this.panel.getBoundingClientRect();
|
|
1160
|
+
const isOverPanel = currentX >= panelRect.left && currentX <= panelRect.right &&
|
|
1161
|
+
currentY >= panelRect.top && currentY <= panelRect.bottom;
|
|
1162
|
+
|
|
1163
|
+
if ( isOverPanel ) {
|
|
1164
|
+
|
|
1165
|
+
windowPanel.style.opacity = '0.5';
|
|
1166
|
+
this.panel.style.outline = '2px solid var(--accent-color)';
|
|
1167
|
+
|
|
1168
|
+
} else {
|
|
1169
|
+
|
|
1170
|
+
windowPanel.style.opacity = '';
|
|
1171
|
+
this.panel.style.outline = '';
|
|
1172
|
+
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
const onDragEnd = ( e ) => {
|
|
1178
|
+
|
|
1179
|
+
if ( ! isDragging ) return;
|
|
1180
|
+
|
|
1181
|
+
isDragging = false;
|
|
1182
|
+
header.style.cursor = '';
|
|
1183
|
+
windowPanel.style.opacity = '';
|
|
1184
|
+
this.panel.style.outline = '';
|
|
1185
|
+
|
|
1186
|
+
// Check if dropped over the inspector panel
|
|
1187
|
+
const currentX = e.clientX;
|
|
1188
|
+
const currentY = e.clientY;
|
|
1189
|
+
|
|
1190
|
+
if ( currentX !== undefined && currentY !== undefined ) {
|
|
1191
|
+
|
|
1192
|
+
const panelRect = this.panel.getBoundingClientRect();
|
|
1193
|
+
const isOverPanel = currentX >= panelRect.left && currentX <= panelRect.right &&
|
|
1194
|
+
currentY >= panelRect.top && currentY <= panelRect.bottom;
|
|
1195
|
+
|
|
1196
|
+
if ( isOverPanel && tab ) {
|
|
1197
|
+
|
|
1198
|
+
// Reattach the tab
|
|
1199
|
+
this.reattachTab( tab );
|
|
1200
|
+
|
|
1201
|
+
} else {
|
|
1202
|
+
|
|
1203
|
+
// Save layout after moving detached window
|
|
1204
|
+
this.saveLayout();
|
|
1205
|
+
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
header.removeEventListener( 'pointermove', onDragMove );
|
|
1211
|
+
header.removeEventListener( 'pointerup', onDragEnd );
|
|
1212
|
+
header.removeEventListener( 'pointercancel', onDragEnd );
|
|
1213
|
+
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
header.addEventListener( 'pointerdown', ( e ) => {
|
|
1217
|
+
|
|
1218
|
+
onDragStart( e );
|
|
1219
|
+
header.addEventListener( 'pointermove', onDragMove );
|
|
1220
|
+
header.addEventListener( 'pointerup', onDragEnd );
|
|
1221
|
+
header.addEventListener( 'pointercancel', onDragEnd );
|
|
1222
|
+
|
|
1223
|
+
} );
|
|
1224
|
+
|
|
1225
|
+
header.style.cursor = 'grab';
|
|
1226
|
+
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
setupDetachedWindowResize( windowPanel, resizerTop, resizerRight, resizerBottom, resizerLeft, resizerCorner ) {
|
|
1230
|
+
|
|
1231
|
+
const minWidth = 250;
|
|
1232
|
+
const minHeight = 150;
|
|
1233
|
+
|
|
1234
|
+
const setupResizer = ( resizer, direction ) => {
|
|
1235
|
+
|
|
1236
|
+
let isResizing = false;
|
|
1237
|
+
let startX, startY, startWidth, startHeight, startLeft, startTop;
|
|
1238
|
+
|
|
1239
|
+
const onResizeStart = ( e ) => {
|
|
1240
|
+
|
|
1241
|
+
e.preventDefault();
|
|
1242
|
+
e.stopPropagation();
|
|
1243
|
+
isResizing = true;
|
|
1244
|
+
|
|
1245
|
+
// Bring window to front when resizing
|
|
1246
|
+
this.bringWindowToFront( windowPanel );
|
|
1247
|
+
|
|
1248
|
+
resizer.setPointerCapture( e.pointerId );
|
|
1249
|
+
|
|
1250
|
+
startX = e.clientX;
|
|
1251
|
+
startY = e.clientY;
|
|
1252
|
+
startWidth = windowPanel.offsetWidth;
|
|
1253
|
+
startHeight = windowPanel.offsetHeight;
|
|
1254
|
+
startLeft = windowPanel.offsetLeft;
|
|
1255
|
+
startTop = windowPanel.offsetTop;
|
|
1256
|
+
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
const onResizeMove = ( e ) => {
|
|
1260
|
+
|
|
1261
|
+
if ( ! isResizing ) return;
|
|
1262
|
+
|
|
1263
|
+
e.preventDefault();
|
|
1264
|
+
|
|
1265
|
+
const currentX = e.clientX;
|
|
1266
|
+
const currentY = e.clientY;
|
|
1267
|
+
|
|
1268
|
+
const deltaX = currentX - startX;
|
|
1269
|
+
const deltaY = currentY - startY;
|
|
1270
|
+
|
|
1271
|
+
const windowWidth = window.innerWidth;
|
|
1272
|
+
const windowHeight = window.innerHeight;
|
|
1273
|
+
|
|
1274
|
+
if ( direction === 'right' || direction === 'corner' ) {
|
|
1275
|
+
|
|
1276
|
+
const newWidth = startWidth + deltaX;
|
|
1277
|
+
const maxWidth = windowWidth - startLeft;
|
|
1278
|
+
|
|
1279
|
+
if ( newWidth >= minWidth && newWidth <= maxWidth ) {
|
|
1280
|
+
|
|
1281
|
+
windowPanel.style.width = `${ newWidth }px`;
|
|
1282
|
+
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if ( direction === 'bottom' || direction === 'corner' ) {
|
|
1288
|
+
|
|
1289
|
+
const newHeight = startHeight + deltaY;
|
|
1290
|
+
const maxHeight = windowHeight - startTop;
|
|
1291
|
+
|
|
1292
|
+
if ( newHeight >= minHeight && newHeight <= maxHeight ) {
|
|
1293
|
+
|
|
1294
|
+
windowPanel.style.height = `${ newHeight }px`;
|
|
1295
|
+
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if ( direction === 'left' ) {
|
|
1301
|
+
|
|
1302
|
+
const newWidth = startWidth - deltaX;
|
|
1303
|
+
const maxLeft = startLeft + startWidth - minWidth;
|
|
1304
|
+
|
|
1305
|
+
if ( newWidth >= minWidth ) {
|
|
1306
|
+
|
|
1307
|
+
const newLeft = startLeft + deltaX;
|
|
1308
|
+
|
|
1309
|
+
if ( newLeft >= 0 && newLeft <= maxLeft ) {
|
|
1310
|
+
|
|
1311
|
+
windowPanel.style.width = `${ newWidth }px`;
|
|
1312
|
+
windowPanel.style.left = `${ newLeft }px`;
|
|
1313
|
+
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
if ( direction === 'top' ) {
|
|
1321
|
+
|
|
1322
|
+
const newHeight = startHeight - deltaY;
|
|
1323
|
+
const maxTop = startTop + startHeight - minHeight;
|
|
1324
|
+
|
|
1325
|
+
if ( newHeight >= minHeight ) {
|
|
1326
|
+
|
|
1327
|
+
const newTop = startTop + deltaY;
|
|
1328
|
+
|
|
1329
|
+
if ( newTop >= 0 && newTop <= maxTop ) {
|
|
1330
|
+
|
|
1331
|
+
windowPanel.style.height = `${ newHeight }px`;
|
|
1332
|
+
windowPanel.style.top = `${ newTop }px`;
|
|
1333
|
+
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
const onResizeEnd = () => {
|
|
1343
|
+
|
|
1344
|
+
isResizing = false;
|
|
1345
|
+
|
|
1346
|
+
resizer.removeEventListener( 'pointermove', onResizeMove );
|
|
1347
|
+
resizer.removeEventListener( 'pointerup', onResizeEnd );
|
|
1348
|
+
resizer.removeEventListener( 'pointercancel', onResizeEnd );
|
|
1349
|
+
|
|
1350
|
+
// Save layout after resizing detached window
|
|
1351
|
+
this.saveLayout();
|
|
1352
|
+
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
resizer.addEventListener( 'pointerdown', ( e ) => {
|
|
1356
|
+
|
|
1357
|
+
onResizeStart( e );
|
|
1358
|
+
resizer.addEventListener( 'pointermove', onResizeMove );
|
|
1359
|
+
resizer.addEventListener( 'pointerup', onResizeEnd );
|
|
1360
|
+
resizer.addEventListener( 'pointercancel', onResizeEnd );
|
|
1361
|
+
|
|
1362
|
+
} );
|
|
1363
|
+
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// Setup all resizers
|
|
1367
|
+
setupResizer( resizerTop, 'top' );
|
|
1368
|
+
setupResizer( resizerRight, 'right' );
|
|
1369
|
+
setupResizer( resizerBottom, 'bottom' );
|
|
1370
|
+
setupResizer( resizerLeft, 'left' );
|
|
1371
|
+
setupResizer( resizerCorner, 'corner' );
|
|
1372
|
+
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
reattachTab( tab ) {
|
|
1376
|
+
|
|
1377
|
+
if ( ! tab.isDetached ) return;
|
|
1378
|
+
|
|
1379
|
+
if ( tab.detachedWindow ) {
|
|
1380
|
+
|
|
1381
|
+
const index = this.detachedWindows.indexOf( tab.detachedWindow );
|
|
1382
|
+
|
|
1383
|
+
if ( index > - 1 ) {
|
|
1384
|
+
|
|
1385
|
+
this.detachedWindows.splice( index, 1 );
|
|
1386
|
+
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
if ( tab.detachedWindow.panel.parentNode ) {
|
|
1390
|
+
|
|
1391
|
+
tab.detachedWindow.panel.parentNode.removeChild( tab.detachedWindow.panel );
|
|
1392
|
+
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
tab.detachedWindow = null;
|
|
1396
|
+
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
tab.isDetached = false;
|
|
1400
|
+
|
|
1401
|
+
// Get all tabs and sort by their original index to determine the correct order
|
|
1402
|
+
const allTabs = Object.values( this.tabs );
|
|
1403
|
+
const allTabsSorted = allTabs
|
|
1404
|
+
.filter( t => t.originalIndex !== undefined && t.isVisible )
|
|
1405
|
+
.sort( ( a, b ) => a.originalIndex - b.originalIndex );
|
|
1406
|
+
|
|
1407
|
+
// Get currently attached tab buttons
|
|
1408
|
+
const currentButtons = Array.from( this.tabsContainer.children );
|
|
1409
|
+
|
|
1410
|
+
// Find the correct position for this tab
|
|
1411
|
+
let insertIndex = 0;
|
|
1412
|
+
for ( const t of allTabsSorted ) {
|
|
1413
|
+
|
|
1414
|
+
if ( t.id === tab.id ) {
|
|
1415
|
+
|
|
1416
|
+
break;
|
|
1417
|
+
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Count only non-detached tabs that come before this one
|
|
1421
|
+
if ( ! t.isDetached ) {
|
|
1422
|
+
|
|
1423
|
+
insertIndex ++;
|
|
1424
|
+
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Insert the button at the correct position
|
|
1430
|
+
if ( insertIndex >= currentButtons.length || currentButtons.length === 0 ) {
|
|
1431
|
+
|
|
1432
|
+
// If insert index is beyond current buttons, or no buttons exist, append at the end
|
|
1433
|
+
this.tabsContainer.appendChild( tab.button );
|
|
1434
|
+
|
|
1435
|
+
} else {
|
|
1436
|
+
|
|
1437
|
+
// Insert before the button at the insert index
|
|
1438
|
+
this.tabsContainer.insertBefore( tab.button, currentButtons[ insertIndex ] );
|
|
1439
|
+
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
this.contentWrapper.appendChild( tab.content );
|
|
1443
|
+
|
|
1444
|
+
this.setActiveTab( tab.id );
|
|
1445
|
+
|
|
1446
|
+
// Update panel size after reattaching
|
|
1447
|
+
this.updatePanelSize();
|
|
1448
|
+
|
|
1449
|
+
this.saveLayout();
|
|
1450
|
+
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
setActiveTab( id ) {
|
|
1454
|
+
|
|
1455
|
+
if ( this.activeTabId && this.tabs[ this.activeTabId ] && ! this.tabs[ this.activeTabId ].isDetached ) {
|
|
1456
|
+
|
|
1457
|
+
this.tabs[ this.activeTabId ].setActive( false );
|
|
1458
|
+
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
this.activeTabId = id;
|
|
1462
|
+
|
|
1463
|
+
if ( this.tabs[ id ] ) {
|
|
1464
|
+
|
|
1465
|
+
this.tabs[ id ].setActive( true );
|
|
1466
|
+
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
togglePanel() {
|
|
1472
|
+
|
|
1473
|
+
this.panel.classList.toggle( 'visible' );
|
|
1474
|
+
this.toggleButton.classList.toggle( 'hidden' );
|
|
1475
|
+
|
|
1476
|
+
const isVisible = this.panel.classList.contains( 'visible' );
|
|
1477
|
+
|
|
1478
|
+
if ( isVisible ) {
|
|
1479
|
+
|
|
1480
|
+
// Save mini-panel state before hiding
|
|
1481
|
+
this.savedMiniPanelState = {
|
|
1482
|
+
isVisible: this.miniPanel.classList.contains( 'visible' ),
|
|
1483
|
+
activeTabId: null,
|
|
1484
|
+
contentMap: {}
|
|
1485
|
+
};
|
|
1486
|
+
|
|
1487
|
+
// Find which tab was active in mini-panel
|
|
1488
|
+
this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
|
|
1489
|
+
|
|
1490
|
+
if ( content.style.display !== 'none' && content.firstChild ) {
|
|
1491
|
+
|
|
1492
|
+
// Find the tab that owns this content
|
|
1493
|
+
Object.values( this.tabs ).forEach( tab => {
|
|
1494
|
+
|
|
1495
|
+
if ( tab.miniContent === content ) {
|
|
1496
|
+
|
|
1497
|
+
this.savedMiniPanelState.activeTabId = tab.id;
|
|
1498
|
+
// Move content back to main panel
|
|
1499
|
+
tab.content.appendChild( content.firstChild );
|
|
1500
|
+
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
} );
|
|
1504
|
+
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
} );
|
|
1508
|
+
|
|
1509
|
+
// Hide mini-panel temporarily
|
|
1510
|
+
this.miniPanel.classList.remove( 'visible' );
|
|
1511
|
+
|
|
1512
|
+
// Hide all mini-panel contents
|
|
1513
|
+
this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
|
|
1514
|
+
|
|
1515
|
+
content.style.display = 'none';
|
|
1516
|
+
|
|
1517
|
+
} );
|
|
1518
|
+
|
|
1519
|
+
// Remove active state from builtin buttons
|
|
1520
|
+
this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
|
|
1521
|
+
|
|
1522
|
+
btn.classList.remove( 'active' );
|
|
1523
|
+
|
|
1524
|
+
} );
|
|
1525
|
+
|
|
1526
|
+
} else {
|
|
1527
|
+
|
|
1528
|
+
// Restore mini-panel state when minimizing
|
|
1529
|
+
if ( this.savedMiniPanelState && this.savedMiniPanelState.isVisible && this.savedMiniPanelState.activeTabId ) {
|
|
1530
|
+
|
|
1531
|
+
const tab = this.tabs[ this.savedMiniPanelState.activeTabId ];
|
|
1532
|
+
|
|
1533
|
+
if ( tab && tab.miniContent && tab.builtinButton ) {
|
|
1534
|
+
|
|
1535
|
+
// Restore mini-panel visibility
|
|
1536
|
+
this.miniPanel.classList.add( 'visible' );
|
|
1537
|
+
tab.miniContent.style.display = 'block';
|
|
1538
|
+
tab.builtinButton.classList.add( 'active' );
|
|
1539
|
+
|
|
1540
|
+
// Move content back to mini-panel
|
|
1541
|
+
const actualContent = tab.content.querySelector( '.list-scroll-wrapper, .profiler-content > *' );
|
|
1542
|
+
|
|
1543
|
+
if ( actualContent ) {
|
|
1544
|
+
|
|
1545
|
+
tab.miniContent.appendChild( actualContent );
|
|
1546
|
+
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
this.detachedWindows.forEach( detachedWindow => {
|
|
1556
|
+
|
|
1557
|
+
if ( isVisible ) {
|
|
1558
|
+
|
|
1559
|
+
detachedWindow.panel.style.opacity = '';
|
|
1560
|
+
detachedWindow.panel.style.visibility = '';
|
|
1561
|
+
detachedWindow.panel.style.pointerEvents = '';
|
|
1562
|
+
|
|
1563
|
+
} else {
|
|
1564
|
+
|
|
1565
|
+
detachedWindow.panel.style.opacity = '0';
|
|
1566
|
+
detachedWindow.panel.style.visibility = 'hidden';
|
|
1567
|
+
detachedWindow.panel.style.pointerEvents = 'none';
|
|
1568
|
+
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
} );
|
|
1572
|
+
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
togglePosition() {
|
|
1576
|
+
|
|
1577
|
+
const newPosition = this.position === 'bottom' ? 'right' : 'bottom';
|
|
1578
|
+
this.setPosition( newPosition );
|
|
1579
|
+
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
setPosition( targetPosition ) {
|
|
1583
|
+
|
|
1584
|
+
if ( this.position === targetPosition ) return;
|
|
1585
|
+
|
|
1586
|
+
this.panel.style.transition = 'none';
|
|
1587
|
+
|
|
1588
|
+
// Check if panel is currently maximized
|
|
1589
|
+
const isMaximized = this.panel.classList.contains( 'maximized' );
|
|
1590
|
+
|
|
1591
|
+
if ( targetPosition === 'right' ) {
|
|
1592
|
+
|
|
1593
|
+
this.position = 'right';
|
|
1594
|
+
this.floatingBtn.classList.add( 'active' );
|
|
1595
|
+
this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><path d="M3 15h18"></path></svg>';
|
|
1596
|
+
this.floatingBtn.title = 'Switch to Bottom';
|
|
1597
|
+
|
|
1598
|
+
// Apply right position styles
|
|
1599
|
+
this.panel.classList.remove( 'position-bottom' );
|
|
1600
|
+
this.panel.classList.add( 'position-right' );
|
|
1601
|
+
this.panel.style.bottom = '';
|
|
1602
|
+
this.panel.style.top = '0';
|
|
1603
|
+
this.panel.style.right = '0';
|
|
1604
|
+
this.panel.style.left = '';
|
|
1605
|
+
|
|
1606
|
+
// Apply size based on maximized state
|
|
1607
|
+
if ( isMaximized ) {
|
|
1608
|
+
|
|
1609
|
+
this.panel.style.width = '100vw';
|
|
1610
|
+
this.panel.style.height = '100%';
|
|
1611
|
+
|
|
1612
|
+
} else {
|
|
1613
|
+
|
|
1614
|
+
this.panel.style.width = `${ this.lastWidthRight }px`;
|
|
1615
|
+
this.panel.style.height = '100%';
|
|
1616
|
+
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
} else {
|
|
1620
|
+
|
|
1621
|
+
this.position = 'bottom';
|
|
1622
|
+
this.floatingBtn.classList.remove( 'active' );
|
|
1623
|
+
this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>';
|
|
1624
|
+
this.floatingBtn.title = 'Switch to Right Side';
|
|
1625
|
+
|
|
1626
|
+
// Apply bottom position styles
|
|
1627
|
+
this.panel.classList.remove( 'position-right' );
|
|
1628
|
+
this.panel.classList.add( 'position-bottom' );
|
|
1629
|
+
this.panel.style.top = '';
|
|
1630
|
+
this.panel.style.right = '';
|
|
1631
|
+
this.panel.style.bottom = '0';
|
|
1632
|
+
this.panel.style.left = '0';
|
|
1633
|
+
|
|
1634
|
+
// Apply size based on maximized state
|
|
1635
|
+
if ( isMaximized ) {
|
|
1636
|
+
|
|
1637
|
+
this.panel.style.width = '100%';
|
|
1638
|
+
this.panel.style.height = '100vh';
|
|
1639
|
+
|
|
1640
|
+
} else {
|
|
1641
|
+
|
|
1642
|
+
this.panel.style.width = '100%';
|
|
1643
|
+
this.panel.style.height = `${ this.lastHeightBottom }px`;
|
|
1644
|
+
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
// Re-enable transition after a brief delay
|
|
1650
|
+
setTimeout( () => {
|
|
1651
|
+
|
|
1652
|
+
this.panel.style.transition = '';
|
|
1653
|
+
|
|
1654
|
+
}, 50 );
|
|
1655
|
+
|
|
1656
|
+
// Update panel size based on visible tabs
|
|
1657
|
+
this.updatePanelSize();
|
|
1658
|
+
|
|
1659
|
+
// Save layout after position change
|
|
1660
|
+
this.saveLayout();
|
|
1661
|
+
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
saveLayout() {
|
|
1665
|
+
|
|
1666
|
+
const layout = {
|
|
1667
|
+
position: this.position,
|
|
1668
|
+
lastHeightBottom: this.lastHeightBottom,
|
|
1669
|
+
lastWidthRight: this.lastWidthRight,
|
|
1670
|
+
activeTabId: this.activeTabId,
|
|
1671
|
+
detachedTabs: []
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
// Save detached windows state
|
|
1675
|
+
this.detachedWindows.forEach( detachedWindow => {
|
|
1676
|
+
|
|
1677
|
+
const tab = detachedWindow.tab;
|
|
1678
|
+
const panel = detachedWindow.panel;
|
|
1679
|
+
|
|
1680
|
+
// Get position values, ensuring they're valid numbers
|
|
1681
|
+
const left = parseFloat( panel.style.left ) || panel.offsetLeft || 0;
|
|
1682
|
+
const top = parseFloat( panel.style.top ) || panel.offsetTop || 0;
|
|
1683
|
+
const width = panel.offsetWidth;
|
|
1684
|
+
const height = panel.offsetHeight;
|
|
1685
|
+
|
|
1686
|
+
layout.detachedTabs.push( {
|
|
1687
|
+
tabId: tab.id,
|
|
1688
|
+
originalIndex: tab.originalIndex !== undefined ? tab.originalIndex : 0,
|
|
1689
|
+
left: left,
|
|
1690
|
+
top: top,
|
|
1691
|
+
width: width,
|
|
1692
|
+
height: height
|
|
1693
|
+
} );
|
|
1694
|
+
|
|
1695
|
+
} );
|
|
1696
|
+
|
|
1697
|
+
try {
|
|
1698
|
+
|
|
1699
|
+
localStorage.setItem( 'profiler-layout', JSON.stringify( layout ) );
|
|
1700
|
+
|
|
1701
|
+
} catch ( e ) {
|
|
1702
|
+
|
|
1703
|
+
console.warn( 'Failed to save profiler layout:', e );
|
|
1704
|
+
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
loadLayout() {
|
|
1710
|
+
|
|
1711
|
+
try {
|
|
1712
|
+
|
|
1713
|
+
const savedLayout = localStorage.getItem( 'profiler-layout' );
|
|
1714
|
+
|
|
1715
|
+
if ( ! savedLayout ) return;
|
|
1716
|
+
|
|
1717
|
+
const layout = JSON.parse( savedLayout );
|
|
1718
|
+
|
|
1719
|
+
// Constrain detached tabs positions to current screen bounds
|
|
1720
|
+
if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {
|
|
1721
|
+
|
|
1722
|
+
const windowWidth = window.innerWidth;
|
|
1723
|
+
const windowHeight = window.innerHeight;
|
|
1724
|
+
|
|
1725
|
+
layout.detachedTabs = layout.detachedTabs.map( detachedTabData => {
|
|
1726
|
+
|
|
1727
|
+
let { left, top, width, height } = detachedTabData;
|
|
1728
|
+
|
|
1729
|
+
// Ensure width and height are within bounds
|
|
1730
|
+
if ( width > windowWidth ) {
|
|
1731
|
+
|
|
1732
|
+
width = windowWidth - 100; // Leave some margin
|
|
1733
|
+
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
if ( height > windowHeight ) {
|
|
1737
|
+
|
|
1738
|
+
height = windowHeight - 100; // Leave some margin
|
|
1739
|
+
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Allow window to extend half its width/height outside the screen
|
|
1743
|
+
const halfWidth = width / 2;
|
|
1744
|
+
const halfHeight = height / 2;
|
|
1745
|
+
|
|
1746
|
+
// Constrain horizontal position (allow half width to extend beyond right edge)
|
|
1747
|
+
if ( left + width > windowWidth + halfWidth ) {
|
|
1748
|
+
|
|
1749
|
+
left = windowWidth + halfWidth - width;
|
|
1750
|
+
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Constrain horizontal position (allow half width to extend beyond left edge)
|
|
1754
|
+
if ( left < - halfWidth ) {
|
|
1755
|
+
|
|
1756
|
+
left = - halfWidth;
|
|
1757
|
+
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Constrain vertical position (allow half height to extend beyond bottom edge)
|
|
1761
|
+
if ( top + height > windowHeight + halfHeight ) {
|
|
1762
|
+
|
|
1763
|
+
top = windowHeight + halfHeight - height;
|
|
1764
|
+
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Constrain vertical position (allow half height to extend beyond top edge)
|
|
1768
|
+
if ( top < - halfHeight ) {
|
|
1769
|
+
|
|
1770
|
+
top = - halfHeight;
|
|
1771
|
+
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
return {
|
|
1775
|
+
...detachedTabData,
|
|
1776
|
+
left,
|
|
1777
|
+
top,
|
|
1778
|
+
width,
|
|
1779
|
+
height
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
} );
|
|
1783
|
+
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// Restore position and dimensions
|
|
1787
|
+
if ( layout.position ) {
|
|
1788
|
+
|
|
1789
|
+
this.position = layout.position;
|
|
1790
|
+
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if ( layout.lastHeightBottom ) {
|
|
1794
|
+
|
|
1795
|
+
this.lastHeightBottom = layout.lastHeightBottom;
|
|
1796
|
+
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if ( layout.lastWidthRight ) {
|
|
1800
|
+
|
|
1801
|
+
this.lastWidthRight = layout.lastWidthRight;
|
|
1802
|
+
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
// Constrain saved dimensions to current screen bounds
|
|
1806
|
+
const windowWidth = window.innerWidth;
|
|
1807
|
+
const windowHeight = window.innerHeight;
|
|
1808
|
+
|
|
1809
|
+
if ( this.lastHeightBottom > windowHeight - 50 ) {
|
|
1810
|
+
|
|
1811
|
+
this.lastHeightBottom = windowHeight - 50;
|
|
1812
|
+
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
if ( this.lastWidthRight > windowWidth - 50 ) {
|
|
1816
|
+
|
|
1817
|
+
this.lastWidthRight = windowWidth - 50;
|
|
1818
|
+
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// Apply the saved position after shell is set up
|
|
1822
|
+
if ( this.position === 'right' ) {
|
|
1823
|
+
|
|
1824
|
+
this.floatingBtn.classList.add( 'active' );
|
|
1825
|
+
this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><path d="M3 15h18"></path></svg>';
|
|
1826
|
+
this.floatingBtn.title = 'Switch to Bottom';
|
|
1827
|
+
|
|
1828
|
+
this.panel.classList.remove( 'position-bottom' );
|
|
1829
|
+
this.panel.classList.add( 'position-right' );
|
|
1830
|
+
this.panel.style.bottom = '';
|
|
1831
|
+
this.panel.style.top = '0';
|
|
1832
|
+
this.panel.style.right = '0';
|
|
1833
|
+
this.panel.style.left = '';
|
|
1834
|
+
this.panel.style.width = `${ this.lastWidthRight }px`;
|
|
1835
|
+
this.panel.style.height = '100%';
|
|
1836
|
+
|
|
1837
|
+
} else {
|
|
1838
|
+
|
|
1839
|
+
this.panel.style.height = `${ this.lastHeightBottom }px`;
|
|
1840
|
+
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if ( layout.activeTabId ) {
|
|
1844
|
+
|
|
1845
|
+
const willBeDetached = layout.detachedTabs &&
|
|
1846
|
+
layout.detachedTabs.some( dt => dt.tabId === layout.activeTabId );
|
|
1847
|
+
|
|
1848
|
+
if ( willBeDetached ) {
|
|
1849
|
+
|
|
1850
|
+
this.setActiveTab( layout.activeTabId );
|
|
1851
|
+
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {
|
|
1857
|
+
|
|
1858
|
+
this.pendingDetachedTabs = layout.detachedTabs;
|
|
1859
|
+
this.restoreDetachedTabs();
|
|
1860
|
+
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
// Update panel size after loading layout
|
|
1864
|
+
this.updatePanelSize();
|
|
1865
|
+
|
|
1866
|
+
} catch ( e ) {
|
|
1867
|
+
|
|
1868
|
+
console.warn( 'Failed to load profiler layout:', e );
|
|
1869
|
+
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
restoreDetachedTabs() {
|
|
1875
|
+
|
|
1876
|
+
if ( ! this.pendingDetachedTabs || this.pendingDetachedTabs.length === 0 ) return;
|
|
1877
|
+
|
|
1878
|
+
this.pendingDetachedTabs.forEach( detachedTabData => {
|
|
1879
|
+
|
|
1880
|
+
const tab = this.tabs[ detachedTabData.tabId ];
|
|
1881
|
+
|
|
1882
|
+
if ( ! tab || tab.isDetached ) return;
|
|
1883
|
+
|
|
1884
|
+
// Restore originalIndex if saved
|
|
1885
|
+
if ( detachedTabData.originalIndex !== undefined ) {
|
|
1886
|
+
|
|
1887
|
+
tab.originalIndex = detachedTabData.originalIndex;
|
|
1888
|
+
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if ( tab.button.parentNode ) {
|
|
1892
|
+
|
|
1893
|
+
tab.button.parentNode.removeChild( tab.button );
|
|
1894
|
+
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
if ( tab.content.parentNode ) {
|
|
1898
|
+
|
|
1899
|
+
tab.content.parentNode.removeChild( tab.content );
|
|
1900
|
+
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
const detachedWindow = this.createDetachedWindow( tab, 0, 0 );
|
|
1904
|
+
|
|
1905
|
+
detachedWindow.panel.style.left = `${ detachedTabData.left }px`;
|
|
1906
|
+
detachedWindow.panel.style.top = `${ detachedTabData.top }px`;
|
|
1907
|
+
detachedWindow.panel.style.width = `${ detachedTabData.width }px`;
|
|
1908
|
+
detachedWindow.panel.style.height = `${ detachedTabData.height }px`;
|
|
1909
|
+
|
|
1910
|
+
// Constrain window to bounds after restoring position and size
|
|
1911
|
+
this.constrainWindowToBounds( detachedWindow.panel );
|
|
1912
|
+
|
|
1913
|
+
this.detachedWindows.push( detachedWindow );
|
|
1914
|
+
|
|
1915
|
+
tab.isDetached = true;
|
|
1916
|
+
tab.detachedWindow = detachedWindow;
|
|
1917
|
+
|
|
1918
|
+
} );
|
|
1919
|
+
|
|
1920
|
+
this.pendingDetachedTabs = null;
|
|
1921
|
+
|
|
1922
|
+
// Update maxZIndex to be higher than all existing windows
|
|
1923
|
+
this.detachedWindows.forEach( detachedWindow => {
|
|
1924
|
+
|
|
1925
|
+
const currentZIndex = parseInt( getComputedStyle( detachedWindow.panel ).zIndex ) || 0;
|
|
1926
|
+
if ( currentZIndex > this.maxZIndex ) {
|
|
1927
|
+
|
|
1928
|
+
this.maxZIndex = currentZIndex;
|
|
1929
|
+
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
} );
|
|
1933
|
+
|
|
1934
|
+
const needsNewActiveTab = ! this.activeTabId ||
|
|
1935
|
+
! this.tabs[ this.activeTabId ] ||
|
|
1936
|
+
this.tabs[ this.activeTabId ].isDetached ||
|
|
1937
|
+
! this.tabs[ this.activeTabId ].isVisible;
|
|
1938
|
+
|
|
1939
|
+
if ( needsNewActiveTab ) {
|
|
1940
|
+
|
|
1941
|
+
const tabIds = Object.keys( this.tabs );
|
|
1942
|
+
const availableTabs = tabIds.filter( id =>
|
|
1943
|
+
! this.tabs[ id ].isDetached &&
|
|
1944
|
+
this.tabs[ id ].isVisible
|
|
1945
|
+
);
|
|
1946
|
+
|
|
1947
|
+
if ( availableTabs.length > 0 ) {
|
|
1948
|
+
|
|
1949
|
+
const buttons = Array.from( this.tabsContainer.children );
|
|
1950
|
+
const orderedTabIds = buttons.map( btn => {
|
|
1951
|
+
|
|
1952
|
+
return Object.keys( this.tabs ).find( id => this.tabs[ id ].button === btn );
|
|
1953
|
+
|
|
1954
|
+
} ).filter( id =>
|
|
1955
|
+
id !== undefined &&
|
|
1956
|
+
! this.tabs[ id ].isDetached &&
|
|
1957
|
+
this.tabs[ id ].isVisible
|
|
1958
|
+
);
|
|
1959
|
+
|
|
1960
|
+
this.setActiveTab( orderedTabIds[ 0 ] || availableTabs[ 0 ] );
|
|
1961
|
+
|
|
1962
|
+
} else {
|
|
1963
|
+
|
|
1964
|
+
this.activeTabId = null;
|
|
1965
|
+
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// Update panel size after restoring detached tabs
|
|
1971
|
+
this.updatePanelSize();
|
|
167
1972
|
|
|
168
1973
|
}
|
|
169
1974
|
|