Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion djangoproject/scss/_console-tabs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,48 @@
>section {
display: none;
text-align: left;
position: relative;

.highlight {
margin-top: 0px;
margin-top: 0;
display: flex;
align-items: center;

pre {
flex: 1;
margin: 0;
}
}

.btn-clipboard {
cursor: pointer;
padding: 8px 12px;
flex-shrink: 0;

i {
color: var(--code-fg);
opacity: 0.6;
transition: opacity 200ms ease;

&:hover {
opacity: 1;
color: var(--link-color);
}
}

.clipboard-success {
@include monospace;
font-size: 80%;
color: var(--link-color);
margin-right: 6px;
white-space: nowrap;
line-height: 1;

&.fade-out {
opacity: 0;
transition: opacity 400ms;
}
}
}
}

Expand Down
41 changes: 32 additions & 9 deletions djangoproject/static/js/djangoproject.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,24 +102,38 @@ window.addEventListener('keydown', function (e) {
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
});

// Add copy buttons to code snippets
// Add copy buttons to code snippets and console tabs
(function () {
const button_el = document.createElement('span');

button_el.classList.add('btn-clipboard');
button_el.setAttribute('title', 'Copy this code');
button_el.innerHTML = '<i class="icon icon-clipboard"></i>';

const selector = '.snippet-filename, .code-block-caption';
document
.querySelectorAll('.snippet-filename, .code-block-caption')
.forEach(function (el) {
el.insertBefore(button_el.cloneNode(true), null);
});

document.querySelectorAll('.console-block > section').forEach(function (el) {
const highlight_el = el.querySelector('.highlight');

document.querySelectorAll(selector).forEach(function (el) {
el.insertBefore(button_el.cloneNode(true), null);
if (highlight_el) {
highlight_el.appendChild(button_el.cloneNode(true));
}
});
})();

// Attach copy functionality to dynamically-created buttons
// Attach copy functionality to clipboard buttons
document.querySelectorAll('.btn-clipboard').forEach(function (el) {
el.addEventListener('click', function () {
const existing_el = this.querySelector('.clipboard-success');

if (existing_el) {
existing_el.remove();
}

const success_el = document.createElement('span');

success_el.classList.add('clipboard-success');
Expand All @@ -130,25 +144,34 @@ document.querySelectorAll('.btn-clipboard').forEach(function (el) {
this.remove();
});

function on_success(el) {
function on_success() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add an unused argument here and on line 145?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Just to clarify — I actually removed the unused el parameter here, since it wasn't being used (the functions access success_el via closure instead). Happy to keep it if you prefer consistency with other patterns in the codebase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, thanks! Sorry for my misunderstanding here.

success_el.innerText = 'Copied!';

setTimeout(function () {
success_el.classList.add('fade-out');
}, 1000);
}

function on_error(el) {
function on_error() {
success_el.innerText = 'Could not copy!';

setTimeout(function () {
success_el.classList.add('fade-out');
}, 5000);
}

const text = this.parentElement.nextElementSibling.textContent.trim();
let text;
const console_section = this.closest('.console-block > section');
const prompt_regex = /^\$ |^\.\.\.\\>/gm;

if (console_section) {
const pre_el = console_section.querySelector('.highlight pre');
text = pre_el.textContent.replace(prompt_regex, '');
} else {
text = this.parentElement.nextElementSibling.textContent;
}

navigator.clipboard.writeText(text).then(on_success, on_error);
navigator.clipboard.writeText(text.trim()).then(on_success, on_error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this trim call needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .trim() prevents a trailing newline, so the command doesn't execute immediately upon paste.

});
});

Expand Down