forked from anandgeorge/jq-console
-
Notifications
You must be signed in to change notification settings - Fork 0
/
jqconsole.coffee
1279 lines (1116 loc) · 43.9 KB
/
jqconsole.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
###
Copyrights 2011, the repl.it project.
Licensed under the MIT license
###
# Shorthand for jQuery.
$ = jQuery
# The states in which the console can be.
STATE_INPUT = 0
STATE_OUTPUT = 1
STATE_PROMPT = 2
# Key code values.
KEY_ENTER = 13
KEY_TAB = 9
KEY_DELETE = 46
KEY_BACKSPACE = 8
KEY_LEFT = 37
KEY_RIGHT = 39
KEY_UP = 38
KEY_DOWN = 40
KEY_HOME = 36
KEY_END = 35
KEY_PAGE_UP = 33
KEY_PAGE_DOWN = 34
# CSS classes
CLASS_PREFIX = 'jqconsole-'
CLASS_CURSOR = "#{CLASS_PREFIX}cursor"
CLASS_HEADER = "#{CLASS_PREFIX}header"
CLASS_PROMPT = "#{CLASS_PREFIX}prompt"
CLASS_OLD_PROMPT = "#{CLASS_PREFIX}old-prompt"
CLASS_INPUT = "#{CLASS_PREFIX}input"
CLASS_BLURRED = "#{CLASS_PREFIX}blurred"
# Frequently used string literals
E_KEYPRESS = 'keypress'
EMPTY_SPAN = '<span/>'
EMPTY_DIV = '<div/>'
EMPTY_SELECTOR = ':empty'
NEWLINE = '\n'
# Default prompt text for main and continuation prompts.
DEFAULT_PROMPT_LABEL = '>>> '
DEFAULT_PROMPT_CONINUE_LABEL = '... '
# The default number of spaces inserted when indenting.
DEFAULT_INDENT_WIDTH = 2
CLASS_ANSI = "#{CLASS_PREFIX}ansi-"
ESCAPE_CHAR = '\033'
ESCAPE_SYNTAX = /\[(\d*)(?:;(\d*))*m/
class Ansi
COLORS: ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']
constructor: ->
@klasses = [];
_append: (klass) =>
klass = "#{CLASS_ANSI}#{klass}"
if @klasses.indexOf(klass) is -1
@klasses.push klass
_remove: (klasses...) =>
for klass in klasses
if klass in ['fonts', 'color', 'background-color']
@klasses = (cls for cls in @klasses when cls.indexOf(klass) isnt CLASS_ANSI.length)
else
klass = "#{CLASS_ANSI}#{klass}"
@klasses = (cls for cls in @klasses when cls isnt klass)
_color: (i) => @COLORS[i]
_style: (code) =>
code = 0 if code == ''
code = parseInt code
return if isNaN code
switch code
when 0 then @klasses = []
when 1 then @_append 'bold'
when 2 then @_append 'lighter'
when 3 then @_append 'italic'
when 4 then @_append 'underline'
when 5 then @_append 'blink'
when 6 then @_append 'blink-rapid'
when 8 then @_append 'hidden'
when 9 then @_append 'line-through'
when 10 then @_remove 'fonts'
when 11,12,13,14,15,16,17,18,19
@_append "fonts-#{code - 10}"
when 20 then @_append 'fraktur'
when 21 then @_remove 'bold', 'lighter'
when 22 then @_remove 'bold', 'lighter'
when 23 then @_remove 'italic', 'fraktur'
when 24 then @_remove 'underline'
when 25 then @_remove 'blink', 'blink-rapid'
when 28 then @_remove 'hidden'
when 29 then @_remove 'line-through'
when 30,31,32,33,34,35,36,37
@_append 'color-' + @_color code - 30
when 39 then @_remove 'color'
when 40,41,42,43,44,45,46,47
@_append 'background-color-' + @_color code - 40
when 49 then @_remove 'background-color'
when 51 then @_append 'framed'
when 53 then @_append 'overline'
when 54 then @_remove 'framed'
when 55 then @_remove 'overline'
getClasses: => @klasses.join ' '
_openSpan: (text) => "<span class=\"#{@getClasses()}\">#{text}"
_closeSpan: (text) => "#{text}</span>"
stylize: (text) =>
text = @_openSpan text
i = 0
while (i = text.indexOf(ESCAPE_CHAR ,i)) and i isnt -1
if d = text[i...].match ESCAPE_SYNTAX
@_style code for code in d[1...]
text = @_closeSpan(text[0...i]) + @_openSpan text[i + 1 + d[0].length...]
else i++
return @_closeSpan text
# Helper functions
spanHtml = (klass, content) -> "<span class=\"#{klass}\">#{content or ''}</span>"
class JQConsole
# Creates a console.
# @arg container: The DOM element into which the console is inserted.
# @arg header: Text to print at the top of the console on reset. Optional.
# Defaults to an empty string.
# @arg prompt_label: The label to show before the command prompt. Optional.
# Defaults to DEFAULT_PROMPT_LABEL.
# @arg prompt_continue: The label to show before continuation lines of the
# command prompt. Optional. Defaults to DEFAULT_PROMPT_CONINUE_LABEL.
constructor: (@container, header, prompt_label, prompt_continue_label) ->
# Mobile devices supported sniff.
@isMobile = !!navigator.userAgent.match /iPhone|iPad|iPod|Android/i
@isIos = !!navigator.userAgent.match /iPhone|iPad|iPod/i
@isAndroid = !!navigator.userAgent.match /Android/i
@$window = $(window)
# The header written when the console is reset.
@header = header or ''
# The prompt label used by Prompt().
@prompt_label_main = prompt_label or DEFAULT_PROMPT_LABEL
@prompt_label_continue = ' \n' + (prompt_continue_label or
DEFAULT_PROMPT_CONINUE_LABEL)
# How many spaces are inserted when a tab character is pressed.
@indent_width = DEFAULT_INDENT_WIDTH
# By default, the console is in the output state.
@state = STATE_OUTPUT
# A queue of input/prompt operations waiting to be called. The items are
# bound functions ready to be called.
@input_queue = []
# The function to call when input is accepted. Valid only in
# input/prompt mode.
@input_callback = null
# The function to call to determine whether the input should continue to the
# next line.
@multiline_callback = null
# A table of all "recorded" inputs given so far.
@history = []
# The index of the currently selected history item. If this is past the end
# of @history, then the user has not selected a history item.
@history_index = 0
# The command which the user was typing before browsing history. Keeping
# track of this allows us to restore the user's command if they browse the
# history then decide to go back to what they were typing.
@history_new = ''
# Whether the current input operation is using history.
@history_active = false
# A table of custom shortcuts, mapping character codes to callbacks.
@shortcuts = {}
# The main console area. Everything else happens inside this.
@$console = $('<pre class="jqconsole"/>').appendTo @container
@$console.css
position: 'absolute'
top: 0
bottom: 0
right: 0
left: 0
margin: 0
overflow: 'auto'
# Whether the console currently has focus.
@$console_focused = true
# On screen somehow invisible textbox for input.
# Copied from codemirror2, this works for both mobile and desktop browsers.
@$input_container = $(EMPTY_DIV).appendTo @container
@$input_container.css
position: 'relative'
width: 1
height: 0
overflow: 'hidden'
@$input_source = $('<textarea/>')
@$input_source.attr('wrap', 'off').css
position: 'absolute'
width: 2
@$input_source.appendTo @$input_container
@$composition = $(EMPTY_DIV)
@$composition.addClass "#{CLASS_PREFIX}composition"
@$composition.css
display: 'inline'
position: 'relative'
# Hash containing all matching settings
# openings/closings[char] = matching_config
# where char is the opening/closing character.
# clss is an array of classes for fast unhighlighting
# for matching_config see Match method
@matchings =
openings: {}
closings: {}
clss: []
@ansi = new Ansi()
# Prepare console for interaction.
@_InitPrompt()
@_SetupEvents()
@Write @header, CLASS_HEADER
# Save this instance to be accessed if lost.
$(@container).data 'jqconsole', this
# Resets the history into intitial state.
ResetHistory: ->
@history = []
@history_index = 0
@history_current = ''
# Resets the shortcut configuration.
ResetShortcuts: ->
@shortcuts = {}
# Resets the matching configuration.
ResetMatchings: ->
@matchings =
openings: {}
closings: {}
clss: []
# Resets the console to its initial state.
Reset: ->
if @state != STATE_OUTPUT then @ClearPromptText true
@state = STATE_OUTPUT
@input_queue = []
@input_callback = null
@multiline_callback = null
@ResetHistory()
@ResetShortcuts()
@ResetMatchings()
@$prompt.detach()
@$input_container.detach()
@$console.html ''
@$prompt.appendTo @$console
@$input_container.appendTo @container
@Write @header, CLASS_HEADER
return undefined
###------------------------ Shortcut Methods -----------------------------###
# Checks the type/value of key codes passed in for registering/unregistering
# shortcuts and handles accordingly.
_CheckKeyCode: (key_code) ->
if isNaN key_code
key_code = key_code.charCodeAt 0
else
key_code = parseInt key_code, 10
if not (0 < key_code < 256) or isNaN key_code
throw new Error 'Key code must be a number between 0 and 256 exclusive.'
return key_code
# A helper function responsible for calling the register/unregister callback
# twice passing in both the upper and lower case letters.
_LetterCaseHelper: (key_code, callback)->
callback key_code
if 65 <= key_code <= 90 then callback key_code + 32
if 97 <= key_code <= 122 then callback key_code - 32
# Registers a Ctrl+Key shortcut.
# @arg key_code: The code of the key pressing which (when Ctrl is held) will
# trigger this shortcut. If a string is provided, the character code of
# the first character is taken.
# @arg callback: A function called when the shortcut is pressed; "this" will
# point to the JQConsole object.
RegisterShortcut: (key_code, callback) ->
key_code = @_CheckKeyCode key_code
if not callback instanceof Function
throw new Error 'Callback must be a function, not ' + callback + '.'
addShortcut = (key) =>
if key not of @shortcuts then @shortcuts[key] = []
@shortcuts[key].push callback
@_LetterCaseHelper key_code, addShortcut
return undefined
# Removes a Ctrl+Key shortcut from shortcut registry.
# @arg key_code: The code of the key pressing which (when Ctrl is held) will
# trigger this shortcut. If a string is provided, the character code of
# the first character is taken.
# @arg handler: The handler that was used when registering the shortcut,
# if not supplied then all shortcut handlers corrosponding to the key
# would be removed.
UnRegisterShortcut: (key_code, handler) ->
key_code = @_CheckKeyCode key_code
removeShortcut = (key)=>
if key of @shortcuts
if handler
@shortcuts[key].splice @shortcuts[key].indexOf(handler), 1
else
delete @shortcuts[key]
@_LetterCaseHelper key_code, removeShortcut
return undefined
###---------------------- END Shortcut Methods ---------------------------###
# Returns the 0-based number of the column on which the cursor currently is.
GetColumn: ->
@$prompt_cursor.text ''
lines = @$console.text().split NEWLINE
@$prompt_cursor.html ' '
return lines[lines.length - 1].length
# Returns the 0-based number of the line on which the cursor currently is.
GetLine: ->
return @$console.text().split(NEWLINE).length - 1
# Clears the contents of the prompt.
# @arg clear_label: If true, also clears the main prompt label (e.g. ">>>").
ClearPromptText: (clear_label) ->
if @state == STATE_OUTPUT
throw new Error 'ClearPromptText() is not allowed in output state.'
@$prompt_before.html ''
@$prompt_after.html ''
@$prompt_label.text if clear_label then '' else @_SelectPromptLabel false
@$prompt_left.text ''
@$prompt_right.text ''
return undefined
# Returns the contents of the prompt.
# @arg full: If true, also includes the prompt labels (e.g. ">>>").
GetPromptText: (full) ->
if @state == STATE_OUTPUT
throw new Error 'GetPromptText() is not allowed in output state.'
if full
@$prompt_cursor.text ''
text = @$prompt.text()
@$prompt_cursor.html ' '
return text
else
getPromptLines = (node) ->
buffer = []
node.children().each -> buffer.push $(@).children().last().text()
return buffer.join NEWLINE
before = getPromptLines @$prompt_before
if before then before += NEWLINE
current = @$prompt_left.text() + @$prompt_right.text()
after = getPromptLines @$prompt_after
if after then after = NEWLINE + after
return before + current + after
# Sets the contents of the prompt.
# @arg text: The text to put in the prompt. May contain multiple lines.
SetPromptText: (text) ->
if @state == STATE_OUTPUT
throw new Error 'SetPromptText() is not allowed in output state.'
@ClearPromptText false
@_AppendPromptText text
@_ScrollToEnd()
return undefined
# Writes the given text to the console in a <span>, with an optional class.
# @arg text: The text to write.
# @arg cls: The class to give the span containing the text. Optional.
Write: (text, cls, escape=true) ->
if escape
text = @ansi.stylize $(EMPTY_SPAN).text(text).html()
span = $(EMPTY_SPAN).html text
if cls? then span.addClass cls
span.insertBefore @$prompt
@_ScrollToEnd()
# Force reclaculation of the cursor's position.
@$prompt_cursor.detach().insertAfter @$prompt_left
return undefined
# Starts an input operation. If another input or prompt operation is currently
# underway, the new input operation is enqueued and will be called when the
# current operation and all previously enqueued operations finish.
# @arg input_callback: A function called with the user's input when the
# user presses Enter and the input operation is complete.
Input: (input_callback) ->
if @state == STATE_PROMPT
# Input operation has a higher priority, Abort and defer current prompt
# by putting it on top of the queue.
current_input_callback = @input_callback
current_multiline_callback = @multiline_callback
current_history_active = @history_active
current_async_multiline = @async_multiline
@AbortPrompt()
@input_queue.unshift => @Prompt current_history_active,
current_input_callback,
current_multiline_callback,
current_async_multiline
else if @state != STATE_OUTPUT
@input_queue.push => @Input input_callback
return
@history_active = false
@input_callback = input_callback
@multiline_callback = null
@state = STATE_INPUT
@$prompt.attr 'class', CLASS_INPUT
@$prompt_label.text @_SelectPromptLabel false
@Focus()
@_ScrollToEnd()
return undefined
# Starts a command prompt operation. If another input or prompt operation is
# currently underway, the new prompt operation is enqueued and will be called
# when the current operation and all previously enqueued operations finish.
# @arg history_enabled: Whether this input should use history. If true, the
# user can select the input from history, and their input will also be
# added as a new history item.
# @arg result_callback: A function called with the user's input when the
# user presses Enter and the prompt operation is complete.
# @arg multiline_callback: If specified, this function is called when the
# user presses Enter to check whether the input should continue to the
# next line. The function must return one of the following values:
# false: the input operation is completed.
# 0: the input continues to the next line with the current indent.
# N (int): the input continues to the next line, and the current indent
# is adjusted by N, e.g. -2 to unindent two levels.
Prompt: (history_enabled, result_callback, multiline_callback, async_multiline) ->
if @state != STATE_OUTPUT
@input_queue.push =>
@Prompt history_enabled, result_callback, multiline_callback, async_multiline
return
@history_active = history_enabled
@input_callback = result_callback
@multiline_callback = multiline_callback
@async_multiline = async_multiline
@state = STATE_PROMPT
@$prompt.attr 'class', CLASS_PROMPT + ' ' + @ansi.getClasses()
@$prompt_label.text @_SelectPromptLabel false
@Focus()
@_ScrollToEnd()
return undefined
# Aborts the current prompt operation and returns to output mode or the next
# queued input/prompt operation.
AbortPrompt: ->
if @state != STATE_PROMPT
throw new Error 'Cannot abort prompt when not in prompt state.'
@Write @GetPromptText(true) + NEWLINE, CLASS_OLD_PROMPT
@ClearPromptText true
@state = STATE_OUTPUT
@input_callback = @multiline_callback = null
@_CheckInputQueue()
return undefined
# Sets focus on the console's hidden input box so input can be read.
Focus: ->
@$input_source.focus() if not @IsDisabled()
return undefined
# Sets the number of spaces inserted when indenting.
SetIndentWidth: (width) ->
@indent_width = width
# Returns the number of spaces inserted when indenting.
GetIndentWidth: ->
return @indent_width
# Registers character matching settings for a single matching
# @arg open: the openning character
# @arg close: the closing character
# @arg cls: the html class to add to the matched characters
RegisterMatching: (open, close, cls) ->
match_config =
opening_char: open
closing_char: close
cls: cls
@matchings.clss.push(cls)
@matchings.openings[open] = match_config
@matchings.closings[close] = match_config
# Unregisters a character matching. cls is optional.
UnRegisterMatching: (open, close) ->
cls = @matchings.openings[open].cls
delete @matchings.openings[open]
delete @matchings.closings[close]
@matchings.clss.splice @matchings.clss.indexOf(cls), 1
# Dumps the content of the console before the current prompt.
Dump: ->
$elems = @$console.find(".#{CLASS_HEADER}").nextUntil(".#{CLASS_PROMPT}")
return (
for elem in $elems
if $(elem).is ".#{CLASS_OLD_PROMPT}"
$(elem).text().replace /^\s+/, '>>> '
else
$(elem).text()
).join ' '
# Gets the current prompt state.
GetState: ->
return if @state is STATE_INPUT
'input'
else if @state is STATE_OUTPUT
'output'
else
'prompt'
# Disables focus and input on the console.
Disable: ->
@$input_source.attr 'disabled', on
@$input_source.blur();
# Enables focus and input on the console.
Enable: ->
@$input_source.attr 'disabled', off
# Returns true if the console is disabled.
IsDisabled: ->
return Boolean @$input_source.attr 'disabled'
# Moves the cursor to the start of the current prompt line.
# @arg all_lines: If true, moves to the beginning of the first prompt line,
# instead of the beginning of the current.
MoveToStart: (all_lines) ->
@_MoveTo all_lines, true
return undefined
# Moves the cursor to the end of the current prompt line.
MoveToEnd: (all_lines) ->
@_MoveTo all_lines, false
return undefined
###------------------------ Private Methods -------------------------------###
_CheckInputQueue: ->
if @input_queue.length
@input_queue.shift()()
# Creates the movable prompt span. When the console is in input mode, this is
# shown and allows user input. The structure of the spans are as follows:
# $prompt
# $prompt_before
# line1
# prompt_label
# prompt_content
# ...
# lineN
# prompt_label
# prompt_content
# $prompt_current
# $prompt_label
# $prompt_left
# $prompt_cursor
# $prompt_right
# $prompt_after
# line1
# prompt_label
# prompt_content
# ...
# lineN
# prompt_label
# prompt_content
_InitPrompt: ->
# The main prompt container.
@$prompt = $(spanHtml(CLASS_INPUT)).appendTo @$console
# The main divisions of the prompt - the lines before the current line, the
# current line, and the lines after it.
@$prompt_before = $(EMPTY_SPAN).appendTo @$prompt
@$prompt_current = $(EMPTY_SPAN).appendTo @$prompt
@$prompt_after = $(EMPTY_SPAN).appendTo @$prompt
# The subdivisions of the current prompt line - the static prompt label
# (e.g. ">>> "), and the editable text to the left and right of the cursor.
@$prompt_label = $(EMPTY_SPAN).appendTo @$prompt_current
@$prompt_left = $(EMPTY_SPAN).appendTo @$prompt_current
@$prompt_right = $(EMPTY_SPAN).appendTo @$prompt_current
# Needed for the CSS z-index on the cursor to work.
@$prompt_right.css position: 'relative'
# The cursor. A span containing a space that shades its following character.
# If the font of the prompt is not monospace, the content should be set to
# the first character of @$prompt_right to get the appropriate width.
@$prompt_cursor = $(spanHtml(CLASS_CURSOR, ' '))
@$prompt_cursor.insertBefore @$prompt_right
@$prompt_cursor.css
color: 'transparent'
display: 'inline'
zIndex: 0
@$prompt_cursor.css('position', 'absolute') if not @isMobile
# Binds all the required input and focus events.
_SetupEvents: ->
# Click to focus.
if @isMobile
@$console.click (e) =>
e.preventDefault()
@Focus()
else
@$console.mouseup (e) =>
fn = =>
if not window.getSelection().toString()
e.preventDefault()
@Focus()
# Force selection update.
setTimeout fn, 0
# Mark the console with a style when it loses focus.
@$input_source.focus =>
@_ScrollToEnd()
@$console_focused = true
@$console.removeClass CLASS_BLURRED
removeClass = =>
if @$console_focused then @$console.removeClass CLASS_BLURRED
setTimeout removeClass, 100
hideTextInput = =>
if @isIos and @$console_focused then @$input_source.hide()
setTimeout hideTextInput, 500
@$input_source.blur =>
@$console_focused = false
if @isIos then @$input_source.show()
addClass = =>
if not @$console_focused then @$console.addClass CLASS_BLURRED
setTimeout addClass, 100
# Intercept pasting.
paste_event = if $.browser.opera then 'input' else 'paste'
@$input_source.bind paste_event, =>
handlePaste = =>
# Opera fires input on composition end.
return if @in_composition
@_AppendPromptText @$input_source.val()
@$input_source.val ''
@Focus()
# Wait until the browser has handled the paste event before scraping.
setTimeout handlePaste, 0
# Actual key-by-key handling.
@$input_source.keypress @_HandleChar
# FF & opera fires keydown events only once (even when holding) and
# also delegates control keys to keypress that *is* fired more than
# once on holding down the key.
key_event = if $.browser.mozilla or $.browser.opera
E_KEYPRESS
else
'keydown'
@$input_source[key_event] @_HandleKey
@$input_source.keydown @_CheckComposition
# Firefox don't fire any key event for composition characters, so we listen
# for the unstandard composition-events.
if $.browser.mozilla?
@$input_source.bind 'compositionstart', @_StartComposition
@$input_source.bind 'compositionend', @_EndCommposition
@$input_source.bind 'text', @_UpdateComposition
# There is no way to detect compositionstart in opera so we poll for it.
if $.browser.opera?
cb = =>
return if @in_composition
# if there was characters that actually escaped to the input source
# then its most probably a multibyte char.
if @$input_source.val().length
@_StartComposition()
setInterval cb, 200
# Handles a character key press.
# @arg event: The jQuery keyboard Event object to handle.
_HandleChar: (event) =>
# We let the browser take over during output mode.
# Skip everything when a modifier key other than shift is held.
# Allow alt key to pass through for unicode & multibyte characters.
if @state == STATE_OUTPUT or event.metaKey or event.ctrlKey
return true
# IE & Chrome capture non-control characters and Enter.
# Mozilla and Opera capture everything.
# This is the most reliable cross-browser; charCode/keyCode break on Opera.
char_code = event.which
# Skip Enter on IE and Chrome and Tab & backspace on Opera.
# These are handled in _HandleKey().
if char_code in [8, 9, 13] then return false
# Pass control characters which are captured on Mozilla/Safari.
if $.browser.mozilla
if event.keyCode or event.altKey
return true
# Pass control characters which are captured on Opera.
if $.browser.opera
if event.altKey
return true
@$prompt_left.text @$prompt_left.text() + String.fromCharCode char_code
@_ScrollToEnd()
return false
# Handles a key up event and dispatches specific handlers.
# @arg event: The jQuery keyboard Event object to handle.
_HandleKey: (event) =>
# We let the browser take over during output mode.
if @state == STATE_OUTPUT then return true
key = event.keyCode or event.which
# Check for char matching next time the callstack unwinds.
setTimeout $.proxy(@_CheckMatchings, this), 0
# Don't care about alt-modifier.
if event.altKey
return true
# Handle shortcuts.
else if event.ctrlKey or event.metaKey
return @_HandleCtrlShortcut key
else if event.shiftKey
# Shift-modifier shortcut.
switch key
when KEY_ENTER then @_HandleEnter true
when KEY_TAB then @_Unindent()
when KEY_UP then @_MoveUp()
when KEY_DOWN then @_MoveDown()
when KEY_PAGE_UP then @_ScrollUp()
when KEY_PAGE_DOWN then @_ScrollDown()
# Allow other Shift shortcuts to pass through to the browser.
else return true
return false
else
# Not a modifier shortcut.
switch key
when KEY_ENTER then @_HandleEnter false
when KEY_TAB then @_Indent()
when KEY_DELETE then @_Delete false
when KEY_BACKSPACE then @_Backspace false
when KEY_LEFT then @_MoveLeft false
when KEY_RIGHT then @_MoveRight false
when KEY_UP then @_HistoryPrevious()
when KEY_DOWN then @_HistoryNext()
when KEY_HOME then @MoveToStart false
when KEY_END then @MoveToEnd false
when KEY_PAGE_UP then @_ScrollUp()
when KEY_PAGE_DOWN then @_ScrollDown()
# Let any other key continue its way to keypress.
else return true
return false
# Handles a Ctrl+Key shortcut.
# @arg key: The keyCode of the pressed key.
_HandleCtrlShortcut: (key) ->
switch key
when KEY_DELETE then @_Delete true
when KEY_BACKSPACE then @_Backspace true
when KEY_LEFT then @_MoveLeft true
when KEY_RIGHT then @_MoveRight true
when KEY_UP then @_MoveUp()
when KEY_DOWN then @_MoveDown()
when KEY_END then @MoveToEnd true
when KEY_HOME then @MoveToStart true
else
if key of @shortcuts
# Execute custom shortcuts.
handler.call(this) for handler in @shortcuts[key]
return false
else
# Allow unhandled Ctrl shortcuts.
return true
# Block handled shortcuts.
return false
# Handles the user pressing the Enter key.
# @arg shift: Whether the shift key is held.
_HandleEnter: (shift) ->
if shift
@_InsertNewLine true
else
text = @GetPromptText()
continuation = (indent) =>
if indent isnt false
@MoveToEnd true
@_InsertNewLine true
for _ in [0...Math.abs indent]
if indent > 0 then @_Indent() else @_Unindent()
else
# Done with input.
cls_suffix = if @state == STATE_INPUT then 'input' else 'prompt'
@Write @GetPromptText(true) + NEWLINE, "#{CLASS_PREFIX}old-" + cls_suffix
@ClearPromptText true
if @history_active
if not @history.length or @history[@history.length - 1] != text
@history.push text
@history_index = @history.length
@state = STATE_OUTPUT
callback = @input_callback
@input_callback = null
if callback then callback text
@_CheckInputQueue()
if @multiline_callback
if @async_multiline
@multiline_callback text, continuation
else
continuation @multiline_callback text
else
continuation false
# Returns the appropriate variables for usage in methods that depends on the
# direction of the interaction with the console.
_GetDirectionals: (back) ->
$prompt_which = if back then @$prompt_left else @$prompt_right
$prompt_opposite = if back then @$prompt_right else @$prompt_left
$prompt_relative = if back then @$prompt_before else @$prompt_after
$prompt_rel_opposite = if back then @$prompt_after else @$prompt_before
MoveToLimit = if back
$.proxy @MoveToStart, @
else
$.proxy @MoveToEnd, @
MoveDirection = if back
$.proxy @_MoveLeft, @
else
$.proxy @_MoveRight, @
which_end = if back then 'last' else 'first'
where_append = if back then 'prependTo' else 'appendTo'
return {
$prompt_which
$prompt_opposite
$prompt_relative
$prompt_rel_opposite
MoveToLimit
MoveDirection
which_end
where_append
}
# Moves the cursor vertically in the current prompt,
# in the same column. (Used by _MoveUp, _MoveDown)
_VerticalMove: (up) ->
{
$prompt_which
$prompt_opposite
$prompt_relative
MoveToLimit
MoveDirection
} = @_GetDirectionals(up)
if $prompt_relative.is EMPTY_SELECTOR then return
pos = @$prompt_left.text().length
MoveToLimit()
MoveDirection()
text = $prompt_which.text()
$prompt_opposite.text if up then text[pos..] else text[...pos]
$prompt_which.text if up then text[...pos] else text[pos..]
# Moves the cursor to the line above the current one, in the same column.
_MoveUp: ->
@_VerticalMove true
# Moves the cursor to the line below the current one, in the same column.
_MoveDown: ->
@_VerticalMove()
# Moves the cursor horizontally in the current prompt.
# Used by _MoveLeft, _MoveRight
_HorizontalMove: (whole_word, back) ->
{
$prompt_which
$prompt_opposite
$prompt_relative
$prompt_rel_opposite
which_end
where_append
} = @_GetDirectionals(back)
regexp = if back then /\w*\W*$/ else /^\w*\W*/
text = $prompt_which.text()
if text
if whole_word
word = text.match regexp
if not word then return
word = word[0]
tmp = $prompt_opposite.text()
$prompt_opposite.text if back then word + tmp else tmp + word
len = word.length
$prompt_which.text if back then text[...-len] else text[len..]
else
tmp = $prompt_opposite.text()
$prompt_opposite.text if back then text[-1...] + tmp else tmp + text[0]
$prompt_which.text if back then text[...-1] else text[1...]
else if not $prompt_relative.is EMPTY_SELECTOR
$which_line = $(EMPTY_SPAN)[where_append] $prompt_rel_opposite
$which_line.append $(EMPTY_SPAN).text @$prompt_label.text()
$which_line.append $(EMPTY_SPAN).text $prompt_opposite.text()
$opposite_line = $prompt_relative.children()[which_end]().detach()
@$prompt_label.text $opposite_line.children().first().text()
$prompt_which.text $opposite_line.children().last().text()
$prompt_opposite.text ''
# Moves the cursor to the left.
# @arg whole_word: Whether to move by a whole word rather than a character.
_MoveLeft: (whole_word) ->
@_HorizontalMove whole_word, true
# Moves the cursor to the right.
# @arg whole_word: Whether to move by a whole word rather than a character.
_MoveRight: (whole_word) ->
@_HorizontalMove whole_word
# Moves the cursor either to the start or end of the current prompt line(s).
_MoveTo: (all_lines, back) ->
{
$prompt_which
$prompt_opposite
$prompt_relative
MoveToLimit
MoveDirection
} = @_GetDirectionals(back)
if all_lines
# Warning! FF 3.6 hangs on is(EMPTY_SELECTOR)
until $prompt_relative.is(EMPTY_SELECTOR) and $prompt_which.text() == ''
MoveToLimit false
MoveDirection false
else
$prompt_opposite.text @$prompt_left.text() + @$prompt_right.text()
$prompt_which.text ''
# Deletes the character or word following the cursor.
# @arg whole_word: Whether to delete a whole word rather than a character.
_Delete: (whole_word) ->
text = @$prompt_right.text()
if text
if whole_word
word = text.match /^\w*\W*/
if not word then return
word = word[0]
@$prompt_right.text text[word.length...]
else
@$prompt_right.text text[1...]
else if not @$prompt_after.is EMPTY_SELECTOR
$lower_line = @$prompt_after.children().first().detach()
@$prompt_right.text $lower_line.children().last().text()
# Deletes the character or word preceding the cursor.
# @arg whole_word: Whether to delete a whole word rather than a character.
_Backspace: (whole_word) ->
setTimeout $.proxy(@_ScrollToEnd, @), 0
text = @$prompt_left.text()
if text
if whole_word
word = text.match /\w*\W*$/
if not word then return
word = word[0]
@$prompt_left.text text[...-word.length]
else
@$prompt_left.text text[...-1]
else if not @$prompt_before.is EMPTY_SELECTOR
$upper_line = @$prompt_before.children().last().detach()