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,32 @@ 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 ;
945+ menuX = centerX - menuWidth / 2 ;
946+ menuY = baseY + this . cellDimensions . height + 40 ;
814947
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 ) ) ;
819-
820- // Ensure menu stays within terminal bounds vertically
821- const maxY = containerRect . height - menuHeight - 10 ; // padding from bottom
948+ // If menu would overflow below, prefer placing it above selection.
949+ const maxY = containerRect . height - menuHeight - 10 ;
822950 if ( menuY > maxY ) {
823- // If menu would go below terminal, position it above the selection
824951 const topY =
825952 startPos && endPos ? Math . min ( startPos . y , endPos . y ) : baseY ;
826953 menuY = topY - menuHeight - 10 ;
827954 }
955+ } else {
956+ menuX = ( containerRect . width - menuWidth ) / 2 ;
957+ menuY = containerRect . height - menuHeight - 20 ;
958+ }
828959
829- // Final bounds check
830- menuY = Math . max ( 10 , Math . min ( menuY , maxY ) ) ;
960+ const minX = 10 ;
961+ const maxX = containerRect . width - menuWidth - 10 ;
962+ menuX = Math . max ( minX , Math . min ( menuX , maxX ) ) ;
831963
832- this . contextMenu . style . left = `${ menuX } px` ;
833- this . contextMenu . style . top = `${ menuY } px` ;
834- this . contextMenu . style . display = "flex" ;
835- }
964+ const minY = 10 ;
965+ const maxY = containerRect . height - menuHeight - 10 ;
966+ menuY = Math . max ( minY , Math . min ( menuY , maxY ) ) ;
967+
968+ this . contextMenu . style . left = `${ menuX } px` ;
969+ this . contextMenu . style . top = `${ menuY } px` ;
970+ this . contextMenu . style . display = "flex" ;
836971 }
837972
838973 createContextMenu ( ) {
@@ -843,7 +978,10 @@ export default class TerminalTouchSelection {
843978 const menuItems = [
844979 { label : strings [ "copy" ] , action : this . copySelection . bind ( this ) } ,
845980 { label : strings [ "paste" ] , action : this . pasteFromClipboard . bind ( this ) } ,
846- { label : "More..." , action : this . showMoreOptions . bind ( this ) } ,
981+ {
982+ label : `${ strings [ "more" ] || "More" } ...` ,
983+ action : this . showMoreOptions . bind ( this ) ,
984+ } ,
847985 ] ;
848986
849987 menuItems . forEach ( ( item ) => {
@@ -932,10 +1070,96 @@ export default class TerminalTouchSelection {
9321070 }
9331071 }
9341072
1073+ selectAllText ( ) {
1074+ if ( ! this . terminal ?. selectAll ) return ;
1075+ this . terminal . selectAll ( ) ;
1076+ this . currentSelection = this . terminal . getSelection ( ) ;
1077+ this . isSelecting = ! ! this . currentSelection ;
1078+ this . selectionStart = null ;
1079+ this . selectionEnd = null ;
1080+ this . hideHandles ( ) ;
1081+
1082+ if ( this . options . showContextMenu && this . currentSelection ) {
1083+ this . showContextMenu ( ) ;
1084+ }
1085+ }
1086+
1087+ getMoreOptionsContext ( ) {
1088+ return {
1089+ terminal : this . terminal ,
1090+ touchSelection : this ,
1091+ selection : this . currentSelection || this . terminal . getSelection ( ) ,
1092+ clearSelection : ( ) => this . forceClearSelection ( ) ,
1093+ copySelection : ( ) => this . copySelection ( ) ,
1094+ pasteFromClipboard : ( ) => this . pasteFromClipboard ( ) ,
1095+ selectAll : ( ) => this . selectAllText ( ) ,
1096+ } ;
1097+ }
1098+
1099+ getResolvedMoreOptions ( ) {
1100+ ensureDefaultMoreOption ( ) ;
1101+ const context = this . getMoreOptionsContext ( ) ;
1102+
1103+ return [ ...terminalMoreOptions . values ( ) ]
1104+ . map ( ( option ) => {
1105+ const label = resolveMoreOptionLabel ( option , context ) ;
1106+ if ( ! label ) return null ;
1107+
1108+ return {
1109+ ...option ,
1110+ label,
1111+ disabled : ! isMoreOptionEnabled ( option , context ) ,
1112+ } ;
1113+ } )
1114+ . filter ( Boolean ) ;
1115+ }
1116+
1117+ async executeMoreOption ( option ) {
1118+ if ( ! option || typeof option . action !== "function" || option . disabled ) {
1119+ if ( this . isSelecting && this . options . showContextMenu ) {
1120+ this . showContextMenu ( ) ;
1121+ }
1122+ return ;
1123+ }
1124+
1125+ try {
1126+ await option . action ( this . getMoreOptionsContext ( ) ) ;
1127+ } catch ( error ) {
1128+ console . error (
1129+ `[TerminalTouchSelection] Failed to execute more option '${ option . id } '.` ,
1130+ error ,
1131+ ) ;
1132+ window . toast ?. ( "Failed to execute action." ) ;
1133+ } finally {
1134+ if ( this . isSelecting && this . options . showContextMenu ) {
1135+ this . showContextMenu ( ) ;
1136+ }
1137+ }
1138+ }
1139+
9351140 showMoreOptions ( ) {
936- // Implement additional options if needed
937- window . toast ( "More options are not implemented yet." ) ;
938- this . forceClearSelection ( ) ;
1141+ const moreOptions = this . getResolvedMoreOptions ( ) ;
1142+ if ( ! moreOptions . length ) return ;
1143+
1144+ const items = moreOptions . map ( ( option ) => ( {
1145+ value : option . id ,
1146+ text : option . label ,
1147+ icon : option . icon ,
1148+ disabled : option . disabled ,
1149+ } ) ) ;
1150+
1151+ this . hideContextMenu ( true ) ;
1152+
1153+ select ( strings [ "more" ] || "More" , items , true )
1154+ . then ( ( selectedId ) => {
1155+ const option = moreOptions . find ( ( entry ) => entry . id === selectedId ) ;
1156+ return this . executeMoreOption ( option ) ;
1157+ } )
1158+ . catch ( ( ) => {
1159+ if ( this . isSelecting && this . options . showContextMenu ) {
1160+ this . showContextMenu ( ) ;
1161+ }
1162+ } ) ;
9391163 }
9401164
9411165 clearSelection ( ) {
0 commit comments