@@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
8181 }
8282
8383 _applyFormatting ( ) {
84+ const showLineNumbers = SettingsStore . getValue ( "showCodeLineNumbers" ) ;
8485 this . activateSpoilers ( [ this . _content . current ] ) ;
8586
8687 // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
@@ -91,29 +92,136 @@ export default class TextualBody extends React.Component {
9192 this . calculateUrlPreview ( ) ;
9293
9394 if ( this . props . mxEvent . getContent ( ) . format === "org.matrix.custom.html" ) {
94- const blocks = ReactDOM . findDOMNode ( this ) . getElementsByTagName ( "code" ) ;
95- if ( blocks . length > 0 ) {
96- // Do this asynchronously: parsing code takes time and we don't
97- // need to block the DOM update on it.
98- setTimeout ( ( ) => {
99- if ( this . _unmounted ) return ;
100- for ( let i = 0 ; i < blocks . length ; i ++ ) {
101- if ( SettingsStore . getValue ( "enableSyntaxHighlightLanguageDetection" ) ) {
102- highlight . highlightBlock ( blocks [ i ] ) ;
103- } else {
104- // Only syntax highlight if there's a class starting with language-
105- const classes = blocks [ i ] . className . split ( / \s + / ) . filter ( function ( cl ) {
106- return cl . startsWith ( 'language-' ) && ! cl . startsWith ( 'language-_' ) ;
107- } ) ;
108-
109- if ( classes . length != 0 ) {
110- highlight . highlightBlock ( blocks [ i ] ) ;
111- }
112- }
95+ // Handle expansion and add buttons
96+ const pres = ReactDOM . findDOMNode ( this ) . getElementsByTagName ( "pre" ) ;
97+ if ( pres . length > 0 ) {
98+ for ( let i = 0 ; i < pres . length ; i ++ ) {
99+ // Wrap a div around <pre> so that the copy button can be correctly positioned
100+ // when the <pre> overflows and is scrolled horizontally.
101+ const div = this . _wrapInDiv ( pres [ i ] ) ;
102+ this . _handleCodeBlockExpansion ( pres [ i ] ) ;
103+ this . _addCodeExpansionButton ( div , pres [ i ] ) ;
104+ this . _addCodeCopyButton ( div ) ;
105+ if ( showLineNumbers ) {
106+ this . _addLineNumbers ( pres [ i ] ) ;
113107 }
114- } , 10 ) ;
108+ }
109+ }
110+ // Highlight code
111+ const codes = ReactDOM . findDOMNode ( this ) . getElementsByTagName ( "code" ) ;
112+ if ( codes . length > 0 ) {
113+ for ( let i = 0 ; i < codes . length ; i ++ ) {
114+ // Do this asynchronously: parsing code takes time and we don't
115+ // need to block the DOM update on it.
116+ setTimeout ( ( ) => {
117+ if ( this . _unmounted ) return ;
118+ for ( let i = 0 ; i < pres . length ; i ++ ) {
119+ this . _highlightCode ( codes [ i ] ) ;
120+ }
121+ } , 10 ) ;
122+ }
123+ }
124+ }
125+ }
126+
127+ _addCodeExpansionButton ( div , pre ) {
128+ // Calculate how many percent does the pre element take up.
129+ // If it's less than 30% we don't add the expansion button.
130+ const percentageOfViewport = pre . offsetHeight / window . innerHeight * 100 ;
131+ if ( percentageOfViewport < 30 ) return ;
132+
133+ const button = document . createElement ( "span" ) ;
134+ button . className = "mx_EventTile_button " ;
135+ if ( pre . className == "mx_EventTile_collapsedCodeBlock" ) {
136+ button . className += "mx_EventTile_expandButton" ;
137+ } else {
138+ button . className += "mx_EventTile_collapseButton" ;
139+ }
140+
141+ button . onclick = async ( ) => {
142+ button . className = "mx_EventTile_button " ;
143+ if ( pre . className == "mx_EventTile_collapsedCodeBlock" ) {
144+ pre . className = "" ;
145+ button . className += "mx_EventTile_collapseButton" ;
146+ } else {
147+ pre . className = "mx_EventTile_collapsedCodeBlock" ;
148+ button . className += "mx_EventTile_expandButton" ;
149+ }
150+
151+ // By expanding/collapsing we changed
152+ // the height, therefore we call this
153+ this . props . onHeightChanged ( ) ;
154+ } ;
155+
156+ div . appendChild ( button ) ;
157+ }
158+
159+ _addCodeCopyButton ( div ) {
160+ const button = document . createElement ( "span" ) ;
161+ button . className = "mx_EventTile_button mx_EventTile_copyButton " ;
162+
163+ // Check if expansion button exists. If so
164+ // we put the copy button to the bottom
165+ const expansionButtonExists = div . getElementsByClassName ( "mx_EventTile_button" ) ;
166+ if ( expansionButtonExists . length > 0 ) button . className += "mx_EventTile_buttonBottom" ;
167+
168+ button . onclick = async ( ) => {
169+ const copyCode = button . parentNode . getElementsByTagName ( "code" ) [ 0 ] ;
170+ const successful = await copyPlaintext ( copyCode . textContent ) ;
171+
172+ const buttonRect = button . getBoundingClientRect ( ) ;
173+ const GenericTextContextMenu = sdk . getComponent ( 'context_menus.GenericTextContextMenu' ) ;
174+ const { close} = ContextMenu . createMenu ( GenericTextContextMenu , {
175+ ...toRightOf ( buttonRect , 2 ) ,
176+ message : successful ? _t ( 'Copied!' ) : _t ( 'Failed to copy' ) ,
177+ } ) ;
178+ button . onmouseleave = close ;
179+ } ;
180+
181+ div . appendChild ( button ) ;
182+ }
183+
184+ _wrapInDiv ( pre ) {
185+ const div = document . createElement ( "div" ) ;
186+ div . className = "mx_EventTile_pre_container" ;
187+
188+ // Insert containing div in place of <pre> block
189+ pre . parentNode . replaceChild ( div , pre ) ;
190+ // Append <pre> block and copy button to container
191+ div . appendChild ( pre ) ;
192+
193+ return div ;
194+ }
195+
196+ _handleCodeBlockExpansion ( pre ) {
197+ if ( ! SettingsStore . getValue ( "expandCodeByDefault" ) ) {
198+ pre . className = "mx_EventTile_collapsedCodeBlock" ;
199+ }
200+ }
201+
202+ _addLineNumbers ( pre ) {
203+ pre . innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre . innerHTML + '<span></span>' ;
204+ const lineNumbers = pre . getElementsByClassName ( "mx_EventTile_lineNumbers" ) [ 0 ] ;
205+ // Calculate number of lines in pre
206+ const number = pre . innerHTML . split ( / \n / ) . length ;
207+ // Iterate through lines starting with 1 (number of the first line is 1)
208+ for ( let i = 1 ; i < number ; i ++ ) {
209+ lineNumbers . innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>' ;
210+ }
211+ }
212+
213+ _highlightCode ( code ) {
214+ if ( SettingsStore . getValue ( "enableSyntaxHighlightLanguageDetection" ) ) {
215+ highlight . highlightBlock ( code ) ;
216+ } else {
217+ // Only syntax highlight if there's a class starting with language-
218+ const classes = code . className . split ( / \s + / ) . filter ( function ( cl ) {
219+ return cl . startsWith ( 'language-' ) && ! cl . startsWith ( 'language-_' ) ;
220+ } ) ;
221+
222+ if ( classes . length != 0 ) {
223+ highlight . highlightBlock ( code ) ;
115224 }
116- this . _addCodeCopyButton ( ) ;
117225 }
118226 }
119227
@@ -254,38 +362,6 @@ export default class TextualBody extends React.Component {
254362 }
255363 }
256364
257- _addCodeCopyButton ( ) {
258- // Add 'copy' buttons to pre blocks
259- Array . from ( ReactDOM . findDOMNode ( this ) . querySelectorAll ( '.mx_EventTile_body pre' ) ) . forEach ( ( p ) => {
260- const button = document . createElement ( "span" ) ;
261- button . className = "mx_EventTile_copyButton" ;
262- button . onclick = async ( ) => {
263- const copyCode = button . parentNode . getElementsByTagName ( "pre" ) [ 0 ] ;
264- const successful = await copyPlaintext ( copyCode . textContent ) ;
265-
266- const buttonRect = button . getBoundingClientRect ( ) ;
267- const GenericTextContextMenu = sdk . getComponent ( 'context_menus.GenericTextContextMenu' ) ;
268- const { close} = ContextMenu . createMenu ( GenericTextContextMenu , {
269- ...toRightOf ( buttonRect , 2 ) ,
270- message : successful ? _t ( 'Copied!' ) : _t ( 'Failed to copy' ) ,
271- } ) ;
272- button . onmouseleave = close ;
273- } ;
274-
275- // Wrap a div around <pre> so that the copy button can be correctly positioned
276- // when the <pre> overflows and is scrolled horizontally.
277- const div = document . createElement ( "div" ) ;
278- div . className = "mx_EventTile_pre_container" ;
279-
280- // Insert containing div in place of <pre> block
281- p . parentNode . replaceChild ( div , p ) ;
282-
283- // Append <pre> block and copy button to container
284- div . appendChild ( p ) ;
285- div . appendChild ( button ) ;
286- } ) ;
287- }
288-
289365 onCancelClick = event => {
290366 this . setState ( { widgetHidden : true } ) ;
291367 // FIXME: persist this somewhere smarter than local storage
0 commit comments