11/**
22 * Touch Selection for Terminal
33 */
4+ import select from "dialogs/select" ;
45import "./terminalTouchSelection.css" ;
56
7+ const DEFAULT_MORE_OPTION_ID = "__acode_terminal_select_all__" ;
8+ const terminalMoreOptions = new Map ( ) ;
9+ let terminalMoreOptionCounter = 0 ;
10+
11+ function ensureDefaultMoreOption ( ) {
12+ if ( terminalMoreOptions . has ( DEFAULT_MORE_OPTION_ID ) ) return ;
13+
14+ terminalMoreOptions . set ( DEFAULT_MORE_OPTION_ID , {
15+ id : DEFAULT_MORE_OPTION_ID ,
16+ label : ( ) => strings [ "select all" ] || "Select all" ,
17+ icon : "text_format" ,
18+ action : ( { touchSelection } ) => touchSelection . selectAllText ( ) ,
19+ } ) ;
20+ }
21+
22+ function normalizeMoreOption ( option ) {
23+ if ( ! option || typeof option !== "object" || Array . isArray ( option ) ) {
24+ console . warn (
25+ "[TerminalTouchSelection] addMoreOption expects an option object." ,
26+ ) ;
27+ return null ;
28+ }
29+
30+ const id =
31+ option . id != null && option . id !== ""
32+ ? String ( option . id )
33+ : `terminal_more_option_${ ++ terminalMoreOptionCounter } ` ;
34+ const label = option . label ?? option . text ?? option . title ;
35+ const action = option . action || option . onselect || option . onclick ;
36+
37+ if ( ! label ) {
38+ console . warn (
39+ `[TerminalTouchSelection] More option '${ id } ' must provide a label/text/title.` ,
40+ ) ;
41+ return null ;
42+ }
43+
44+ if ( typeof action !== "function" ) {
45+ console . warn (
46+ `[TerminalTouchSelection] More option '${ id } ' must provide an action function.` ,
47+ ) ;
48+ return null ;
49+ }
50+
51+ return {
52+ id,
53+ label,
54+ icon : option . icon || null ,
55+ enabled : option . enabled ,
56+ action,
57+ } ;
58+ }
59+
60+ function resolveMoreOptionLabel ( option , context ) {
61+ try {
62+ const value =
63+ typeof option . label === "function" ? option . label ( context ) : option . label ;
64+ return value == null ? "" : String ( value ) ;
65+ } catch ( error ) {
66+ console . warn (
67+ `[TerminalTouchSelection] Failed to resolve label for option '${ option . id } '.` ,
68+ error ,
69+ ) ;
70+ return "" ;
71+ }
72+ }
73+
74+ function isMoreOptionEnabled ( option , context ) {
75+ try {
76+ if ( typeof option . enabled === "function" ) {
77+ return option . enabled ( context ) !== false ;
78+ }
79+ if ( option . enabled === undefined ) return true ;
80+ return option . enabled !== false ;
81+ } catch ( error ) {
82+ console . warn (
83+ `[TerminalTouchSelection] Failed to resolve enabled state for option '${ option . id } '.` ,
84+ error ,
85+ ) ;
86+ return true ;
87+ }
88+ }
89+
690export default class TerminalTouchSelection {
91+ /**
92+ * Register an option for the "More" menu in touch selection.
93+ * @param {{
94+ * id?: string,
95+ * label?: string|function(object):string,
96+ * text?: string,
97+ * title?: string,
98+ * icon?: string,
99+ * enabled?: boolean|function(object):boolean,
100+ * action?: function(object):void|Promise<void>,
101+ * onselect?: function(object):void|Promise<void>,
102+ * onclick?: function(object):void|Promise<void>
103+ * }} option
104+ * @returns {string|null }
105+ */
106+ static addMoreOption ( option ) {
107+ ensureDefaultMoreOption ( ) ;
108+ const normalized = normalizeMoreOption ( option ) ;
109+ if ( ! normalized ) return null ;
110+ terminalMoreOptions . set ( normalized . id , normalized ) ;
111+ return normalized . id ;
112+ }
113+
114+ /**
115+ * Remove a registered "More" menu option by id.
116+ * @param {string } id
117+ * @returns {boolean }
118+ */
119+ static removeMoreOption ( id ) {
120+ ensureDefaultMoreOption ( ) ;
121+ if ( id == null || id === "" ) return false ;
122+ return terminalMoreOptions . delete ( String ( id ) ) ;
123+ }
124+
125+ /**
126+ * List all registered "More" menu options.
127+ * @returns {Array<object> }
128+ */
129+ static getMoreOptions ( ) {
130+ ensureDefaultMoreOption ( ) ;
131+ return [ ...terminalMoreOptions . values ( ) ] . map ( ( option ) => ( { ...option } ) ) ;
132+ }
133+
7134 constructor ( terminal , container , options = { } ) {
135+ ensureDefaultMoreOption ( ) ;
136+
8137 this . terminal = terminal ;
9138 this . container = container ;
10139 this . options = {
@@ -783,17 +912,27 @@ export default class TerminalTouchSelection {
783912 // Mark that context menu should stay visible
784913 this . contextMenuShouldStayVisible = true ;
785914
786- // Position context menu - center it on selection with viewport bounds checking
787- const startPos = this . terminalCoordsToPixels ( this . selectionStart ) ;
788- const endPos = this . terminalCoordsToPixels ( this . selectionEnd ) ;
915+ // Position context menu - center it on selection (or fallback to center).
916+ const startPos = this . selectionStart
917+ ? this . terminalCoordsToPixels ( this . selectionStart )
918+ : null ;
919+ const endPos = this . selectionEnd
920+ ? this . terminalCoordsToPixels ( this . selectionEnd )
921+ : null ;
922+
923+ const menuWidth = this . contextMenu . offsetWidth || 200 ;
924+ const menuHeight = this . contextMenu . offsetHeight || 50 ;
925+ const containerRect = this . container . getBoundingClientRect ( ) ;
926+
927+ let menuX ;
928+ let menuY ;
789929
790930 if ( startPos || endPos ) {
791- // Use whichever position is available, or center between them
792- let centerX , baseY ;
931+ let centerX ;
932+ let baseY ;
793933
794934 if ( startPos && endPos ) {
795935 centerX = ( startPos . x + endPos . x ) / 2 ;
796- // Position below the lower of the two positions
797936 baseY = Math . max ( startPos . y , endPos . y ) ;
798937 } else if ( startPos ) {
799938 centerX = startPos . x ;
@@ -803,36 +942,24 @@ export default class TerminalTouchSelection {
803942 baseY = endPos . y ;
804943 }
805944
806- const menuWidth = this . contextMenu . offsetWidth || 200 ;
807- const menuHeight = this . contextMenu . offsetHeight || 50 ;
808-
809- const containerRect = this . container . getBoundingClientRect ( ) ;
810-
811- // Calculate initial position
812- let menuX = centerX - menuWidth / 2 ;
813- let menuY = baseY + this . cellDimensions . height + 40 ;
814-
815- // Ensure menu stays within terminal bounds horizontally
816- const minX = 10 ; // padding from left edge
817- const maxX = containerRect . width - menuWidth - 10 ; // padding from right edge
818- menuX = Math . max ( minX , Math . min ( menuX , maxX ) ) ;
945+ menuX = centerX - menuWidth / 2 ;
946+ menuY = baseY + this . cellDimensions . height + 40 ;
947+ } else {
948+ menuX = ( containerRect . width - menuWidth ) / 2 ;
949+ menuY = containerRect . height - menuHeight - 20 ;
950+ }
819951
820- // Ensure menu stays within terminal bounds vertically
821- const maxY = containerRect . height - menuHeight - 10 ; // padding from bottom
822- if ( menuY > maxY ) {
823- // If menu would go below terminal, position it above the selection
824- const topY =
825- startPos && endPos ? Math . min ( startPos . y , endPos . y ) : baseY ;
826- menuY = topY - menuHeight - 10 ;
827- }
952+ const minX = 10 ;
953+ const maxX = containerRect . width - menuWidth - 10 ;
954+ menuX = Math . max ( minX , Math . min ( menuX , maxX ) ) ;
828955
829- // Final bounds check
830- menuY = Math . max ( 10 , Math . min ( menuY , maxY ) ) ;
956+ const minY = 10 ;
957+ const maxY = containerRect . height - menuHeight - 10 ;
958+ menuY = Math . max ( minY , Math . min ( menuY , maxY ) ) ;
831959
832- this . contextMenu . style . left = `${ menuX } px` ;
833- this . contextMenu . style . top = `${ menuY } px` ;
834- this . contextMenu . style . display = "flex" ;
835- }
960+ this . contextMenu . style . left = `${ menuX } px` ;
961+ this . contextMenu . style . top = `${ menuY } px` ;
962+ this . contextMenu . style . display = "flex" ;
836963 }
837964
838965 createContextMenu ( ) {
@@ -843,7 +970,10 @@ export default class TerminalTouchSelection {
843970 const menuItems = [
844971 { label : strings [ "copy" ] , action : this . copySelection . bind ( this ) } ,
845972 { label : strings [ "paste" ] , action : this . pasteFromClipboard . bind ( this ) } ,
846- { label : "More..." , action : this . showMoreOptions . bind ( this ) } ,
973+ {
974+ label : `${ strings [ "more" ] || "More" } ...` ,
975+ action : this . showMoreOptions . bind ( this ) ,
976+ } ,
847977 ] ;
848978
849979 menuItems . forEach ( ( item ) => {
@@ -932,10 +1062,96 @@ export default class TerminalTouchSelection {
9321062 }
9331063 }
9341064
1065+ selectAllText ( ) {
1066+ if ( ! this . terminal ?. selectAll ) return ;
1067+ this . terminal . selectAll ( ) ;
1068+ this . currentSelection = this . terminal . getSelection ( ) ;
1069+ this . isSelecting = ! ! this . currentSelection ;
1070+ this . selectionStart = null ;
1071+ this . selectionEnd = null ;
1072+ this . hideHandles ( ) ;
1073+
1074+ if ( this . options . showContextMenu && this . currentSelection ) {
1075+ this . showContextMenu ( ) ;
1076+ }
1077+ }
1078+
1079+ getMoreOptionsContext ( ) {
1080+ return {
1081+ terminal : this . terminal ,
1082+ touchSelection : this ,
1083+ selection : this . currentSelection || this . terminal . getSelection ( ) ,
1084+ clearSelection : ( ) => this . forceClearSelection ( ) ,
1085+ copySelection : ( ) => this . copySelection ( ) ,
1086+ pasteFromClipboard : ( ) => this . pasteFromClipboard ( ) ,
1087+ selectAll : ( ) => this . selectAllText ( ) ,
1088+ } ;
1089+ }
1090+
1091+ getResolvedMoreOptions ( ) {
1092+ ensureDefaultMoreOption ( ) ;
1093+ const context = this . getMoreOptionsContext ( ) ;
1094+
1095+ return [ ...terminalMoreOptions . values ( ) ]
1096+ . map ( ( option ) => {
1097+ const label = resolveMoreOptionLabel ( option , context ) ;
1098+ if ( ! label ) return null ;
1099+
1100+ return {
1101+ ...option ,
1102+ label,
1103+ disabled : ! isMoreOptionEnabled ( option , context ) ,
1104+ } ;
1105+ } )
1106+ . filter ( Boolean ) ;
1107+ }
1108+
1109+ async executeMoreOption ( option ) {
1110+ if ( ! option || typeof option . action !== "function" || option . disabled ) {
1111+ if ( this . isSelecting && this . options . showContextMenu ) {
1112+ this . showContextMenu ( ) ;
1113+ }
1114+ return ;
1115+ }
1116+
1117+ try {
1118+ await option . action ( this . getMoreOptionsContext ( ) ) ;
1119+ } catch ( error ) {
1120+ console . error (
1121+ `[TerminalTouchSelection] Failed to execute more option '${ option . id } '.` ,
1122+ error ,
1123+ ) ;
1124+ window . toast ?. ( "Failed to execute action." ) ;
1125+ } finally {
1126+ if ( this . isSelecting && this . options . showContextMenu ) {
1127+ this . showContextMenu ( ) ;
1128+ }
1129+ }
1130+ }
1131+
9351132 showMoreOptions ( ) {
936- // Implement additional options if needed
937- window . toast ( "More options are not implemented yet." ) ;
938- this . forceClearSelection ( ) ;
1133+ const moreOptions = this . getResolvedMoreOptions ( ) ;
1134+ if ( ! moreOptions . length ) return ;
1135+
1136+ const items = moreOptions . map ( ( option ) => ( {
1137+ value : option . id ,
1138+ text : option . label ,
1139+ icon : option . icon ,
1140+ disabled : option . disabled ,
1141+ } ) ) ;
1142+
1143+ this . hideContextMenu ( true ) ;
1144+
1145+ select ( strings [ "more" ] || "More" , items , true )
1146+ . then ( ( selectedId ) => {
1147+ const option = moreOptions . find ( ( entry ) => entry . id === selectedId ) ;
1148+ return this . executeMoreOption ( option ) ;
1149+ } )
1150+ . catch ( ( ) => {
1151+ if ( this . isSelecting && this . options . showContextMenu ) {
1152+ this . showContextMenu ( ) ;
1153+ }
1154+ } ) ;
9391155 }
9401156
9411157 clearSelection ( ) {
0 commit comments