mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge pull request #5559 from SimonBrandner/improve-codeblock
Improve displaying of code blocks
This commit is contained in:
commit
6d01de3bfb
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
|
@ -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",
|
||||||
|
@ -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'),
|
||||||
|
Loading…
Reference in New Issue
Block a user