Merge pull request #5559 from SimonBrandner/improve-codeblock

Improve displaying of code blocks
This commit is contained in:
J. Ryan Stinnett 2021-02-08 14:44:48 +00:00 committed by GitHub
commit 6d01de3bfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 190 additions and 61 deletions

View File

@ -134,7 +134,7 @@ limitations under the License.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); mask-image: url('$(res)/img/feather-customised/maximise.svg');
background: $muted-fg-color; background: $muted-fg-color;
} }
} }

View File

@ -35,13 +35,13 @@ limitations under the License.
mask-size: auto 12px; mask-size: auto 12px;
visibility: hidden; visibility: hidden;
background-color: $accent-color; background-color: $accent-color;
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); mask-image: url('$(res)/img/feather-customised/maximise.svg');
} }
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
mask-position: 0 bottom; mask-position: 0 bottom;
margin-bottom: 7px; margin-bottom: 7px;
mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); mask-image: url('$(res)/img/feather-customised/minimise.svg');
} }
&:hover .mx_ViewSourceEvent_toggle { &:hover .mx_ViewSourceEvent_toggle {

View File

@ -491,7 +491,6 @@ $left-gutter: 64px;
// https://github.com/vector-im/vector-web/issues/754 // https://github.com/vector-im/vector-web/issues/754
overflow-x: overlay; overflow-x: overlay;
overflow-y: visible; overflow-y: visible;
max-height: 30vh;
} }
code { code {
@ -500,6 +499,22 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile_lineNumbers {
float: left;
margin: 0 0.5em 0 -1.5em;
color: gray;
}
.mx_EventTile_lineNumber {
text-align: right;
display: block;
padding-left: 1em;
}
.mx_EventTile_collapsedCodeBlock {
max-height: 30vh;
}
.mx_EventTile:hover .mx_EventTile_body pre, .mx_EventTile:hover .mx_EventTile_body pre,
.mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre { .mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre {
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
@ -511,7 +526,7 @@ $left-gutter: 64px;
} }
// Inserted adjacent to <pre> blocks, (See TextualBody) // Inserted adjacent to <pre> blocks, (See TextualBody)
.mx_EventTile_copyButton { .mx_EventTile_button {
position: absolute; position: absolute;
display: inline-block; display: inline-block;
visibility: hidden; visibility: hidden;
@ -520,12 +535,33 @@ $left-gutter: 64px;
right: 6px; right: 6px;
width: 19px; width: 19px;
height: 19px; height: 19px;
mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color; background-color: $message-action-bar-fg-color;
} }
.mx_EventTile_buttonBottom {
top: 31px;
}
.mx_EventTile_copyButton {
mask-image: url($copy-button-url);
}
.mx_EventTile_collapseButton {
mask-size: 75%;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url($collapse-button-url);
}
.mx_EventTile_expandButton {
mask-size: 75%;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url($expand-button-url);
}
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton, .mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton { .mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton,
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_collapseButton,
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_collapseButton,
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_expandButton,
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_expandButton {
visibility: visible; visibility: visible;
} }

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -237,7 +237,8 @@ $event-redacted-border-color: #cccccc;
$event-timestamp-color: #acacac; $event-timestamp-color: #acacac;
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; $copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
// e2e // e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color

View File

@ -237,6 +237,8 @@ $event-redacted-border-color: #cccccc;
$event-timestamp-color: #acacac; $event-timestamp-color: #acacac;
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; $copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
// e2e // e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color

View File

@ -81,6 +81,7 @@ export default class TextualBody extends React.Component {
} }
_applyFormatting() { _applyFormatting() {
const showLineNumbers = SettingsStore.getValue("showCodeLineNumbers");
this.activateSpoilers([this._content.current]); this.activateSpoilers([this._content.current]);
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
@ -91,29 +92,136 @@ export default class TextualBody extends React.Component {
this.calculateUrlPreview(); this.calculateUrlPreview();
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
const blocks = ReactDOM.findDOMNode(this).getElementsByTagName("code"); // Handle expansion and add buttons
if (blocks.length > 0) { const pres = ReactDOM.findDOMNode(this).getElementsByTagName("pre");
// Do this asynchronously: parsing code takes time and we don't if (pres.length > 0) {
// need to block the DOM update on it. for (let i = 0; i < pres.length; i++) {
setTimeout(() => { // Wrap a div around <pre> so that the copy button can be correctly positioned
if (this._unmounted) return; // when the <pre> overflows and is scrolled horizontally.
for (let i = 0; i < blocks.length; i++) { const div = this._wrapInDiv(pres[i]);
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) { this._handleCodeBlockExpansion(pres[i]);
highlight.highlightBlock(blocks[i]); this._addCodeExpansionButton(div, pres[i]);
} else { this._addCodeCopyButton(div);
// Only syntax highlight if there's a class starting with language- if (showLineNumbers) {
const classes = blocks[i].className.split(/\s+/).filter(function(cl) { this._addLineNumbers(pres[i]);
return cl.startsWith('language-') && !cl.startsWith('language-_');
});
if (classes.length != 0) {
highlight.highlightBlock(blocks[i]);
}
}
} }
}, 10); }
}
// Highlight code
const codes = ReactDOM.findDOMNode(this).getElementsByTagName("code");
if (codes.length > 0) {
for (let i = 0; i < codes.length; i++) {
// Do this asynchronously: parsing code takes time and we don't
// need to block the DOM update on it.
setTimeout(() => {
if (this._unmounted) return;
for (let i = 0; i < pres.length; i++) {
this._highlightCode(codes[i]);
}
}, 10);
}
}
}
}
_addCodeExpansionButton(div, pre) {
// Calculate how many percent does the pre element take up.
// If it's less than 30% we don't add the expansion button.
const percentageOfViewport = pre.offsetHeight / window.innerHeight * 100;
if (percentageOfViewport < 30) return;
const button = document.createElement("span");
button.className = "mx_EventTile_button ";
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
button.className += "mx_EventTile_expandButton";
} else {
button.className += "mx_EventTile_collapseButton";
}
button.onclick = async () => {
button.className = "mx_EventTile_button ";
if (pre.className == "mx_EventTile_collapsedCodeBlock") {
pre.className = "";
button.className += "mx_EventTile_collapseButton";
} else {
pre.className = "mx_EventTile_collapsedCodeBlock";
button.className += "mx_EventTile_expandButton";
}
// By expanding/collapsing we changed
// the height, therefore we call this
this.props.onHeightChanged();
};
div.appendChild(button);
}
_addCodeCopyButton(div) {
const button = document.createElement("span");
button.className = "mx_EventTile_button mx_EventTile_copyButton ";
// Check if expansion button exists. If so
// we put the copy button to the bottom
const expansionButtonExists = div.getElementsByClassName("mx_EventTile_button");
if (expansionButtonExists.length > 0) button.className += "mx_EventTile_buttonBottom";
button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("code")[0];
const successful = await copyPlaintext(copyCode.textContent);
const buttonRect = button.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
button.onmouseleave = close;
};
div.appendChild(button);
}
_wrapInDiv(pre) {
const div = document.createElement("div");
div.className = "mx_EventTile_pre_container";
// Insert containing div in place of <pre> block
pre.parentNode.replaceChild(div, pre);
// Append <pre> block and copy button to container
div.appendChild(pre);
return div;
}
_handleCodeBlockExpansion(pre) {
if (!SettingsStore.getValue("expandCodeByDefault")) {
pre.className = "mx_EventTile_collapsedCodeBlock";
}
}
_addLineNumbers(pre) {
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
// Calculate number of lines in pre
const number = pre.innerHTML.split(/\n/).length;
// Iterate through lines starting with 1 (number of the first line is 1)
for (let i = 1; i < number; i++) {
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
}
}
_highlightCode(code) {
if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
highlight.highlightBlock(code);
} else {
// Only syntax highlight if there's a class starting with language-
const classes = code.className.split(/\s+/).filter(function(cl) {
return cl.startsWith('language-') && !cl.startsWith('language-_');
});
if (classes.length != 0) {
highlight.highlightBlock(code);
} }
this._addCodeCopyButton();
} }
} }
@ -254,38 +362,6 @@ export default class TextualBody extends React.Component {
} }
} }
_addCodeCopyButton() {
// Add 'copy' buttons to pre blocks
Array.from(ReactDOM.findDOMNode(this).querySelectorAll('.mx_EventTile_body pre')).forEach((p) => {
const button = document.createElement("span");
button.className = "mx_EventTile_copyButton";
button.onclick = async () => {
const copyCode = button.parentNode.getElementsByTagName("pre")[0];
const successful = await copyPlaintext(copyCode.textContent);
const buttonRect = button.getBoundingClientRect();
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
...toRightOf(buttonRect, 2),
message: successful ? _t('Copied!') : _t('Failed to copy'),
});
button.onmouseleave = close;
};
// Wrap a div around <pre> so that the copy button can be correctly positioned
// when the <pre> overflows and is scrolled horizontally.
const div = document.createElement("div");
div.className = "mx_EventTile_pre_container";
// Insert containing div in place of <pre> block
p.parentNode.replaceChild(div, p);
// Append <pre> block and copy button to container
div.appendChild(p);
div.appendChild(button);
});
}
onCancelClick = event => { onCancelClick = event => {
this.setState({ widgetHidden: true }); this.setState({ widgetHidden: true });
// FIXME: persist this somewhere smarter than local storage // FIXME: persist this somewhere smarter than local storage

View File

@ -47,6 +47,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
'alwaysShowTimestamps', 'alwaysShowTimestamps',
'showRedactions', 'showRedactions',
'enableSyntaxHighlightLanguageDetection', 'enableSyntaxHighlightLanguageDetection',
'expandCodeByDefault',
'showCodeLineNumbers',
'showJoinLeaves', 'showJoinLeaves',
'showAvatarChanges', 'showAvatarChanges',
'showDisplaynameChanges', 'showDisplaynameChanges',

View File

@ -806,6 +806,8 @@
"Always show message timestamps": "Always show message timestamps", "Always show message timestamps": "Always show message timestamps",
"Autoplay GIFs and videos": "Autoplay GIFs and videos", "Autoplay GIFs and videos": "Autoplay GIFs and videos",
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
"Expand code blocks by default": "Expand code blocks by default",
"Show line numbers in code blocks": "Show line numbers in code blocks",
"Show avatars in user and room mentions": "Show avatars in user and room mentions", "Show avatars in user and room mentions": "Show avatars in user and room mentions",
"Enable big emoji in chat": "Enable big emoji in chat", "Enable big emoji in chat": "Enable big emoji in chat",
"Send typing notifications": "Send typing notifications", "Send typing notifications": "Send typing notifications",

View File

@ -305,6 +305,16 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td('Enable automatic language detection for syntax highlighting'), displayName: _td('Enable automatic language detection for syntax highlighting'),
default: false, default: false,
}, },
"expandCodeByDefault": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Expand code blocks by default'),
default: false,
},
"showCodeLineNumbers": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show line numbers in code blocks'),
default: true,
},
"Pill.shouldShowPillAvatar": { "Pill.shouldShowPillAvatar": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show avatars in user and room mentions'), displayName: _td('Show avatars in user and room mentions'),