Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 6d01de3

Browse files
authored
Merge pull request #5559 from SimonBrandner/improve-codeblock
Improve displaying of code blocks
2 parents cf5ec77 + 84eba59 commit 6d01de3

File tree

11 files changed

+190
-61
lines changed

11 files changed

+190
-61
lines changed

res/css/structures/_LeftPanelWidget.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ limitations under the License.
134134
mask-position: center;
135135
mask-size: contain;
136136
mask-repeat: no-repeat;
137-
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
137+
mask-image: url('$(res)/img/feather-customised/maximise.svg');
138138
background: $muted-fg-color;
139139
}
140140
}

res/css/views/messages/_ViewSourceEvent.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ limitations under the License.
3535
mask-size: auto 12px;
3636
visibility: hidden;
3737
background-color: $accent-color;
38-
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
38+
mask-image: url('$(res)/img/feather-customised/maximise.svg');
3939
}
4040

4141
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
4242
mask-position: 0 bottom;
4343
margin-bottom: 7px;
44-
mask-image: url('$(res)/img/feather-customised/widget/minimise.svg');
44+
mask-image: url('$(res)/img/feather-customised/minimise.svg');
4545
}
4646

4747
&:hover .mx_ViewSourceEvent_toggle {

res/css/views/rooms/_EventTile.scss

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,6 @@ $left-gutter: 64px;
491491
// https://github.com/vector-im/vector-web/issues/754
492492
overflow-x: overlay;
493493
overflow-y: visible;
494-
max-height: 30vh;
495494
}
496495

497496
code {
@@ -500,6 +499,22 @@ $left-gutter: 64px;
500499
}
501500
}
502501

502+
.mx_EventTile_lineNumbers {
503+
float: left;
504+
margin: 0 0.5em 0 -1.5em;
505+
color: gray;
506+
}
507+
508+
.mx_EventTile_lineNumber {
509+
text-align: right;
510+
display: block;
511+
padding-left: 1em;
512+
}
513+
514+
.mx_EventTile_collapsedCodeBlock {
515+
max-height: 30vh;
516+
}
517+
503518
.mx_EventTile:hover .mx_EventTile_body pre,
504519
.mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre {
505520
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
@@ -511,7 +526,7 @@ $left-gutter: 64px;
511526
}
512527

513528
// Inserted adjacent to <pre> blocks, (See TextualBody)
514-
.mx_EventTile_copyButton {
529+
.mx_EventTile_button {
515530
position: absolute;
516531
display: inline-block;
517532
visibility: hidden;
@@ -520,12 +535,33 @@ $left-gutter: 64px;
520535
right: 6px;
521536
width: 19px;
522537
height: 19px;
523-
mask-image: url($copy-button-url);
524538
background-color: $message-action-bar-fg-color;
525539
}
540+
.mx_EventTile_buttonBottom {
541+
top: 31px;
542+
}
543+
.mx_EventTile_copyButton {
544+
mask-image: url($copy-button-url);
545+
}
546+
.mx_EventTile_collapseButton {
547+
mask-size: 75%;
548+
mask-position: center;
549+
mask-repeat: no-repeat;
550+
mask-image: url($collapse-button-url);
551+
}
552+
.mx_EventTile_expandButton {
553+
mask-size: 75%;
554+
mask-position: center;
555+
mask-repeat: no-repeat;
556+
mask-image: url($expand-button-url);
557+
}
526558

527559
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
528-
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton {
560+
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton,
561+
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_collapseButton,
562+
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_collapseButton,
563+
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_expandButton,
564+
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_expandButton {
529565
visibility: visible;
530566
}
531567

res/themes/legacy-light/css/_legacy-light.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ $event-redacted-border-color: #cccccc;
237237
$event-timestamp-color: #acacac;
238238

239239
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
240-
240+
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
241+
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
241242

242243
// e2e
243244
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color

res/themes/light/css/_light.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ $event-redacted-border-color: #cccccc;
237237
$event-timestamp-color: #acacac;
238238

239239
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
240+
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
241+
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
240242

241243
// e2e
242244
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color

src/components/views/messages/TextualBody.js

Lines changed: 129 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -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

src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
4747
'alwaysShowTimestamps',
4848
'showRedactions',
4949
'enableSyntaxHighlightLanguageDetection',
50+
'expandCodeByDefault',
51+
'showCodeLineNumbers',
5052
'showJoinLeaves',
5153
'showAvatarChanges',
5254
'showDisplaynameChanges',

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,8 @@
806806
"Always show message timestamps": "Always show message timestamps",
807807
"Autoplay GIFs and videos": "Autoplay GIFs and videos",
808808
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
809+
"Expand code blocks by default": "Expand code blocks by default",
810+
"Show line numbers in code blocks": "Show line numbers in code blocks",
809811
"Show avatars in user and room mentions": "Show avatars in user and room mentions",
810812
"Enable big emoji in chat": "Enable big emoji in chat",
811813
"Send typing notifications": "Send typing notifications",

0 commit comments

Comments
 (0)