x4js 2.3.16 → 2.3.18

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.
@@ -19,13 +19,13 @@ import { Component, ComponentEvent, ComponentEvents, ComponentProps } from '../.
19
19
  import { EventCallback } from '../../core/core_events';
20
20
  import { CSizer } from '../sizers/sizer';
21
21
 
22
- import { bandOf, dockActivate, dockBand, dockFloat, dockMove, dockRestore, dockShow, dropZone, floatOf, isShown, keepInside, sharesOf, shownPanel, sideRect, stackOf, stacksOf, visiblePanels } from './docklayout';
23
- import { DockFloat, DockKinds, DockLayout, DockNode, DockRect, DockStack } from './docklayout';
22
+ import { bandOf, dockActivate, dockBand, dockFloat, dockMove, dockRestore, dockShow, dockSides, dropZone, floatOf, isFlat, isShown, keepInside, sharesOf, shownPanel, sideRect, stackOf, stacksOf, visiblePanels } from './docklayout';
23
+ import { DockFloat, DockKinds, DockLayout, DockNode, DockRect, DockSide, DockStack } from './docklayout';
24
24
 
25
25
  import "./dockingview.module.scss";
26
26
 
27
27
  // the layout is given and read by the host: its types go with the component
28
- export type { DockLayout, DockBand, DockNode, DockSplit, DockStack, DockFloat } from './docklayout';
28
+ export type { DockLayout, DockBand, DockNode, DockSplit, DockStack, DockFloat, DockSide } from './docklayout';
29
29
 
30
30
  /** under this distance, in pixels, a press on a title is a click, not a drag */
31
31
  const DRAG_SLOP = 4;
@@ -39,8 +39,12 @@ const MIN_SIZE = 48;
39
39
  /** what a sizer always leaves to the contents, between the two bands */
40
40
  const MIN_AREA = 80;
41
41
 
42
- /** width of a band whose layout gives none */
42
+ /** width of a band whose layout gives none; height, for the band of the top or of the bottom */
43
43
  const BAND_SIZE = 200;
44
+ const BAND_HEIGHT = 160;
45
+
46
+ /** the bands, in the order they are looked at */
47
+ const SIDES: DockSide[] = ["left", "right", "top", "bottom"];
44
48
 
45
49
  /** smallest size of a panel that starts to float */
46
50
  const FLOAT_MIN_W = 160;
@@ -68,6 +72,9 @@ export interface DockPanel {
68
72
 
69
73
  /** a tool: instead of what the box says. A content never floats. */
70
74
  floating?: boolean;
75
+
76
+ /** a tool: the bands it may be docked in, instead of what the box says */
77
+ sides?: DockSide[];
71
78
  }
72
79
 
73
80
  export interface DockingViewProps extends ComponentProps {
@@ -82,6 +89,14 @@ export interface DockingViewProps extends ComponentProps {
82
89
  /** tools have a button that hides them (see showPanel) */
83
90
  closable?: boolean;
84
91
 
92
+ /**
93
+ * The bands tools may be docked in: "left", "right", "top", "bottom".
94
+ * The left and the right ones when missing. A tool is never offered a
95
+ * place in a band it may not be in, neither beside one of its tools nor
96
+ * as one of their tabs.
97
+ */
98
+ sides?: DockSide[];
99
+
85
100
  /**
86
101
  * A name: the layout is then remembered under it, in the storage of
87
102
  * the application, and found again the next time.
@@ -142,6 +157,11 @@ interface Drag {
142
157
  * dropped anywhere else floats above the rest. With `closable`, its title
143
158
  * has a button that hides it; showPanel() brings it back.
144
159
  *
160
+ * With `sides`, a tool may also be docked above or under the contents, in
161
+ * a band that lies between those of the left and of the right: for what is
162
+ * wide and low (a log, a list of results, a timeline). `sides` says which
163
+ * bands a tool may be in, for all the tools or for one of them.
164
+ *
145
165
  * A content is what is worked on (a document, as the editors of an IDE).
146
166
  * Contents share the room between the bands: a title dragged to a side of
147
167
  * another content is put beside it, to its middle it becomes one of its
@@ -175,7 +195,7 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
175
195
 
176
196
  // the boxes of the layout as it is shown now
177
197
  private nodes = new Map<DockNode, Component>( );
178
- private bands = new Map<"left" | "right", Component>( );
198
+ private bands = new Map<DockSide, Component>( );
179
199
  private floats = new Map<string, Component>( );
180
200
  private area: Component; // the room of the contents, between the bands
181
201
 
@@ -277,7 +297,23 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
277
297
 
278
298
  private kinds( ): DockKinds {
279
299
  const ids = this.props.panels.map( panel => panel.id );
280
- return { tools: ids.filter( id => this.isTool( id ) ), contents: ids.filter( id => !this.isTool( id ) ) };
300
+ const tools = ids.filter( id => this.isTool( id ) );
301
+
302
+ // what a tool says for itself, otherwise what the box says for all
303
+ const sides: Record<string, DockSide[]> = {};
304
+ for( const id of tools ) {
305
+ sides[id] = this.panel( id ).sides ?? this.props.sides;
306
+ }
307
+
308
+ return { tools, contents: ids.filter( id => !this.isTool( id ) ), sides };
309
+ }
310
+
311
+ /**
312
+ * true when a tool may be docked in a band
313
+ */
314
+
315
+ private mayDock( id: string, side: DockSide ): boolean {
316
+ return dockSides( this.kinds( ), id ).includes( side );
281
317
  }
282
318
 
283
319
  private mayFloat( id: string ): boolean {
@@ -316,8 +352,10 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
316
352
  // :: BUILDING ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
317
353
 
318
354
  /**
319
- * Make the boxes of the layout again: the band of the left, the room
320
- * of the contents, the band of the right, and the floating tools.
355
+ * Make the boxes of the layout again: the band of the left, then,
356
+ * one above the other, the band of the top, the room of the contents
357
+ * and the band of the bottom, then the band of the right; and the
358
+ * floating tools.
321
359
  *
322
360
  * What the panels show is not built again: their pages are moved. They
323
361
  * go straight from a place in the document to another one, the park
@@ -346,9 +384,19 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
346
384
  content: content && isShown( content, hidden ) ? this.makeNode( content, titled ) : null,
347
385
  } );
348
386
 
387
+ // between the bands of the sides, which take the whole height
388
+ const middle = new Component( {
389
+ cls: "dock-middle",
390
+ content: [
391
+ ...this.makeBand( "top" ),
392
+ this.area,
393
+ ...this.makeBand( "bottom" ),
394
+ ],
395
+ } );
396
+
349
397
  this.frame.setContent( [
350
398
  ...this.makeBand( "left" ),
351
- this.area,
399
+ middle,
352
400
  ...this.makeBand( "right" ),
353
401
  ...floats.filter( float => !hidden.includes( float.panel ) ).map( float => this.makeFloat( float ) ),
354
402
  ] );
@@ -362,39 +410,58 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
362
410
  }
363
411
 
364
412
  /**
365
- * A band and the sizer that changes its width, in the order they are
366
- * shown; nothing when none of its tools is shown.
413
+ * A band and the sizer that changes its width (its height, for the
414
+ * band of the top or of the bottom), in the order they are shown;
415
+ * nothing when none of its tools is shown.
367
416
  */
368
417
 
369
- private makeBand( side: "left" | "right" ): Component[] {
418
+ private makeBand( side: DockSide ): Component[] {
370
419
  const band = this.layout[side];
371
420
 
372
- if( !band.root || !isShown( band.root, this.layout.hidden ) ) {
421
+ if( !band?.root || !isShown( band.root, this.layout.hidden ) ) {
373
422
  return [];
374
423
  }
375
424
 
425
+ const flat = isFlat( side );
426
+
376
427
  const box = new Component( {
377
428
  cls: `dock-band ${side}`,
378
- width: band.size || BAND_SIZE,
379
429
  content: this.makeNode( band.root, true ),
380
430
  } );
381
431
 
432
+ if( flat ) {
433
+ box.setHeight( band.size || BAND_HEIGHT );
434
+ }
435
+ else {
436
+ box.setWidth( band.size || BAND_SIZE );
437
+ }
438
+
382
439
  this.bands.set( side, box );
383
440
 
384
- // what the band gets is taken from the contents
385
- const sign = side === "left" ? 1 : -1;
441
+ // what the band gets is taken from the contents: the sizer is on
442
+ // the side of the band that faces them
443
+ const first = side === "left" || side === "top";
444
+ const sign = first ? 1 : -1;
386
445
  let size = 0;
387
446
  let max = 0;
388
447
 
389
- const sizer = this.makeSizer( true, ( ) => {
448
+ const sizer = this.makeSizer( !flat, ( ) => {
449
+ const room = this.area.getBoundingRect( );
450
+
390
451
  size = band.size;
391
- max = size + Math.max( 0, this.area.getBoundingRect( ).width - MIN_AREA );
452
+ max = size + Math.max( 0, ( flat ? room.height : room.width ) - MIN_AREA );
392
453
  }, delta => {
393
454
  band.size = Math.round( Math.max( MIN_SIZE, Math.min( size + delta * sign, max ) ) );
394
- box.setWidth( band.size );
455
+
456
+ if( flat ) {
457
+ box.setHeight( band.size );
458
+ }
459
+ else {
460
+ box.setWidth( band.size );
461
+ }
395
462
  } );
396
463
 
397
- return side === "left" ? [box, sizer] : [sizer, box];
464
+ return first ? [box, sizer] : [sizer, box];
398
465
  }
399
466
 
400
467
  private makeNode( node: DockNode, titled: boolean ): Component {
@@ -588,12 +655,23 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
588
655
  visit( this.layout.content, null );
589
656
  }
590
657
 
591
- for( const side of ["left", "right"] as const ) {
658
+ for( const side of SIDES ) {
592
659
  const band = this.layout[side];
593
- const width = Math.round( this.bands.get( side )?.getBoundingRect( ).width ?? 0 );
660
+ if( !band ) {
661
+ continue;
662
+ }
663
+
664
+ // A band that is not shown has no box: its size stays what it
665
+ // was. Otherwise its width, or its height when it lies flat.
666
+ const box = this.bands.get( side );
667
+
668
+ if( box ) {
669
+ const rc = box.getBoundingRect( );
670
+ const size = Math.round( isFlat( side ) ? rc.height : rc.width );
594
671
 
595
- if( width > 0 ) {
596
- band.size = width;
672
+ if( size > 0 ) {
673
+ band.size = size;
674
+ }
597
675
  }
598
676
 
599
677
  if( band.root ) {
@@ -761,8 +839,11 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
761
839
  *
762
840
  * a side of a tool beside it, above or under it
763
841
  * the middle of another tool one of its tabs
764
- * the left or the right of the contents the band of that side, under its tools
842
+ * a side of the contents the band of that side, after its tools
765
843
  * anywhere else it floats, if it may
844
+ *
845
+ * The first three only for a band the tool may be docked in (`sides`):
846
+ * the left and the right ones, unless the host says otherwise.
766
847
  */
767
848
 
768
849
  private dropTool( x: number, y: number ): Drop {
@@ -777,8 +858,10 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
777
858
 
778
859
  const inside = ( rect: DockRect ) => dropZone( rect, x, y, 1 ) !== null;
779
860
 
780
- // the width the tool brings to a band: the one it has now, a third of the box at most
861
+ // the width the tool brings to a band: the one it has now, a third of the box at most;
862
+ // and its height, for a band that lies flat
781
863
  const width = Math.round( Math.max( MIN_SIZE, Math.min( ( float?.w ?? page.w ) || BAND_SIZE, within.w / 3 ) ) );
864
+ const height = Math.round( Math.max( MIN_SIZE, Math.min( ( float?.h ?? page.h ) || BAND_HEIGHT, within.h / 3 ) ) );
782
865
 
783
866
  const floating = ( ): Drop => {
784
867
  if( !this.mayFloat( panel ) ) {
@@ -800,44 +883,52 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
800
883
  return floating( );
801
884
  }
802
885
 
803
- // a tool of a band
804
- for( const stack of [...stacksOf( layout.left.root ), ...stacksOf( layout.right.root )] ) {
805
- const box = this.nodes.get( stack );
806
- const rect = box ? this.rectOf( box ) : null;
807
- const zone = rect ? dropZone( rect, x, y, SIDE_BAND ) : null;
808
-
809
- if( !zone ) {
886
+ // a tool of a band, when the one that is dragged may be in that band
887
+ for( const side of SIDES ) {
888
+ if( !this.mayDock( panel, side ) ) {
810
889
  continue;
811
890
  }
812
891
 
813
- const panels = visiblePanels( stack, hidden );
892
+ for( const stack of stacksOf( layout[side]?.root ) ) {
893
+ const box = this.nodes.get( stack );
894
+ const rect = box ? this.rectOf( box ) : null;
895
+ const zone = rect ? dropZone( rect, x, y, SIDE_BAND ) : null;
814
896
 
815
- if( zone === "center" ) {
816
- // the middle of its own stack: it leaves it
817
- return stack === own ? floating( ) : { rect, apply: ( ) => dockMove( layout, panel, panels[0], "tab" ) };
818
- }
897
+ if( !zone ) {
898
+ continue;
899
+ }
819
900
 
820
- // beside the stack it is alone in: it is already there
821
- if( stack === own && panels.length === 1 ) {
822
- return null;
823
- }
901
+ const panels = visiblePanels( stack, hidden );
824
902
 
825
- // Half of the stack is shown. On the left or on the right, the
826
- // band gets wider instead: the two tools keep their width.
827
- const half = ( zone === "left" || zone === "right" ? rect.w : rect.h ) / 2;
828
- return { rect: sideRect( rect, zone, half ), apply: ( ) => dockMove( layout, panel, panels[0], zone, width ) };
903
+ if( zone === "center" ) {
904
+ // the middle of its own stack: it leaves it
905
+ return stack === own ? floating( ) : { rect, apply: ( ) => dockMove( layout, panel, panels[0], "tab" ) };
906
+ }
907
+
908
+ // beside the stack it is alone in: it is already there
909
+ if( stack === own && panels.length === 1 ) {
910
+ return null;
911
+ }
912
+
913
+ // Half of the stack is shown. On the left or on the right, the
914
+ // band gets wider instead: the two tools keep their width. In
915
+ // a band that lies flat, it is above and under, and its height.
916
+ const half = ( zone === "left" || zone === "right" ? rect.w : rect.h ) / 2;
917
+ return { rect: sideRect( rect, zone, half ), apply: ( ) => dockMove( layout, panel, panels[0], zone, isFlat( side ) ? height : width ) };
918
+ }
829
919
  }
830
920
 
831
- // the left or the right of the contents: the band of that side
921
+ // a side of the contents: the band of that side
832
922
  const area = this.rectOf( this.area );
833
- const side = x < area.x + SIDE_BAND ? "left" : x >= area.x + area.w - SIDE_BAND ? "right" : null;
923
+ const side = this.edgeOf( panel, area, x, y );
834
924
 
835
925
  if( !side ) {
836
926
  return floating( );
837
927
  }
838
928
 
929
+ const flat = isFlat( side );
839
930
  const band = this.bands.get( side );
840
- const root = layout[side].root;
931
+ const root = layout[side]?.root;
841
932
  const shown = stacksOf( root ).filter( stack => isShown( stack, hidden ) );
842
933
 
843
934
  // the only tool of that band: it is already there
@@ -845,14 +936,60 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
845
936
  return null;
846
937
  }
847
938
 
848
- // A band that shows nothing is made beside the contents, as wide as
849
- // the tool is now. Otherwise the tool goes under what the band
850
- // holds: one more row of it, or its lower half.
851
- const rows = root?.type === "split" && root.dir === "column" ? root.children.filter( child => isShown( child, hidden ) ).length : 1;
939
+ // A band that shows nothing is made beside the contents, as big as
940
+ // the tool is now. Otherwise the tool goes after what the band
941
+ // holds: one more row of it, or its lower half; one more column, or
942
+ // its right half, for a band that lies flat.
943
+ const size = flat ? height : width;
944
+ const count = root?.type === "split" && root.dir === ( flat ? "row" : "column" ) ? root.children.filter( child => isShown( child, hidden ) ).length : 1;
852
945
  const bandRect = band ? this.rectOf( band ) : null;
853
- const rect = bandRect ? sideRect( bandRect, "bottom", bandRect.h / ( rows + 1 ) ) : sideRect( area, side, width );
854
946
 
855
- return { rect, apply: ( ) => dockBand( layout, panel, side, band ? 0 : width ) };
947
+ let rect: DockRect;
948
+
949
+ if( !bandRect ) {
950
+ rect = sideRect( area, side, size );
951
+ }
952
+ else if( flat ) {
953
+ rect = sideRect( bandRect, "right", bandRect.w / ( count + 1 ) );
954
+ }
955
+ else {
956
+ rect = sideRect( bandRect, "bottom", bandRect.h / ( count + 1 ) );
957
+ }
958
+
959
+ return { rect, apply: ( ) => dockBand( layout, panel, side, band ? 0 : size ) };
960
+ }
961
+
962
+ /**
963
+ * The side of the contents a point is near, among those the band of
964
+ * which a tool may be docked in; the nearest one, null for none.
965
+ *
966
+ * A point beyond the side counts too: it is between the contents and
967
+ * the band. But it has to be along the side: over a band of another
968
+ * side, it is not near this one.
969
+ */
970
+
971
+ private edgeOf( panel: string, area: DockRect, x: number, y: number ): DockSide {
972
+ const along = ( side: DockSide ) => isFlat( side ) ? x >= area.x && x < area.x + area.w : y >= area.y && y < area.y + area.h;
973
+
974
+ // how deep the point is from each side: 0 on it, 1 where its zone ends
975
+ const depth = ( side: DockSide ): number => {
976
+ switch( side ) {
977
+ case "left": return ( x - area.x ) / SIDE_BAND;
978
+ case "right": return ( area.x + area.w - x ) / SIDE_BAND;
979
+ case "top": return ( y - area.y ) / SIDE_BAND;
980
+ case "bottom": return ( area.y + area.h - y ) / SIDE_BAND;
981
+ }
982
+ };
983
+
984
+ let nearest: DockSide = null;
985
+
986
+ for( const side of SIDES ) {
987
+ if( this.mayDock( panel, side ) && along( side ) && depth( side ) < 1 && ( nearest === null || depth( side ) < depth( nearest ) ) ) {
988
+ nearest = side;
989
+ }
990
+ }
991
+
992
+ return nearest;
856
993
  }
857
994
 
858
995
  /**