Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ordering front end redesign #4595

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4cb004f
feat(new_ui): NEW HTML and CSS and JS
flooie Oct 17, 2024
2ac21c6
feat(opinion.urls): Add/remove new endpoints
flooie Oct 17, 2024
c260b60
feat(opinion.views): Create new view methods
flooie Oct 17, 2024
08a4e86
feat(tests): Update to tests
flooie Oct 17, 2024
bc92162
fix(tests): Fix tests
flooie Oct 18, 2024
be333b3
fix(opinion_page): Remove comments and fix lint
flooie Oct 18, 2024
5b0cf27
feat(printing): Prettify Printing
flooie Oct 18, 2024
7e8a9e2
feat(page-numbers): Fix anon page numbering
flooie Oct 18, 2024
d15e349
refactor(js): Refactor js and css
flooie Oct 22, 2024
353ebd2
fix(urls): Remove details
flooie Oct 22, 2024
e3fa07e
fix(utils): Fix missing data in elastic related cache call
flooie Oct 22, 2024
1ad1040
feat(opinions.html): Tweak to tab display
flooie Oct 22, 2024
dfdca1f
feat(opinions): Some small UI tweaks
flooie Oct 23, 2024
dbe7bf3
Update cl/search/models.py
flooie Oct 24, 2024
d210bdd
Update cl/opinion_page/views.py
flooie Oct 24, 2024
6477b6f
Update cl/opinion_page/views.py
flooie Oct 24, 2024
3d96869
fix(op_pages/test): Lint and typing changes
flooie Oct 24, 2024
70c0c49
fix(search.test): Remove unused import
flooie Oct 24, 2024
1735fae
refactor(cl.opinion_page): Simplify code around waffles
flooie Oct 24, 2024
3c009be
refactor(search.models): Update display citation
flooie Oct 24, 2024
ecd7bb2
fix(cl_opinion_page): Fix typo
flooie Oct 24, 2024
7606b51
fix(cl_opinion_page): Update bot response to empty data
flooie Oct 24, 2024
0117ed0
tests(cl_opinion_page): Tweak citation redirect to use new opinions
flooie Oct 24, 2024
7170885
Update cl/opinion_page/utils.py
flooie Nov 4, 2024
f33d192
Update cl/opinion_page/views.py
flooie Nov 4, 2024
8aa0950
Update cl/opinion_page/utils.py
flooie Nov 4, 2024
97405c8
Update cl/opinion_page/views.py
flooie Nov 4, 2024
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
695 changes: 695 additions & 0 deletions cl/assets/static-global/css/opinions.css

Large diffs are not rendered by default.

35 changes: 26 additions & 9 deletions cl/assets/static-global/css/override.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,30 @@ header {

/* Standard target color. */
*:target {
background-color: lightyellow;
-webkit-animation: target-fade 3s;
-moz-animation: target-fade 3s;
-o-animation: target-fade 3s;
animation: target-fade 3s;
}

@-webkit-keyframes target-fade {
from { background-color: lightyellow; }
to { background-color: transparent; }
}

@-moz-keyframes target-fade {
from { background-color: lightyellow; }
to { background-color: transparent; }
}

@-o-keyframes target-fade {
from { background-color: lightyellow; }
to { background-color: transparent; }
}

@keyframes target-fade {
from { background-color: lightyellow; }
to { background-color: transparent; }
}

.alt {
Expand Down Expand Up @@ -1012,13 +1035,6 @@ closely the content in the book*/
font-size: 1em;
}

#headmatter {
border: 1px rgba(210, 210, 210, 0.55) solid;
padding: 10px;
background: rgba(232, 232, 232, 0.37);
margin: 10px;
}

#headmatter > attorneys, docketnumbers, judges, footnotes, court, decisiondate {
line-height: 2em;
font-size: 14px;
Expand Down Expand Up @@ -1603,7 +1619,7 @@ textarea {


/* Prevent images inside opinion from overflowing */
#opinion-content img {
div.subopinion-content img {
max-width: 100%;
height: auto;
}
Expand Down Expand Up @@ -1723,3 +1739,4 @@ rect.series-segment {
opacity 150ms 150ms ease-in;
transform: translate3d(0, 0, 0);
}

4 changes: 1 addition & 3 deletions cl/assets/static-global/js/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,8 @@ $(document).ready(function () {
if (modal_exist) {
$('#open-modal-on-load').modal();
}

});



// Debounce - rate limit a function
// https://davidwalsh.name/javascript-debounce-function
function debounce(func, wait, immediate) {
Expand Down Expand Up @@ -369,3 +366,4 @@ if (form && button) {
button.disabled = true;
});
}

271 changes: 271 additions & 0 deletions cl/assets/static-global/js/opinions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@

////////////////
// Pagination //
////////////////

// Star pagination weirdness for ANON 2020 dataset -

$('.star-pagination').each(function (index, element) {
if ($(this).attr('pagescheme')) {
// For ANON 2020 this has two sets of numbers but only one can be
// verified with other databses so only showing one
var number = $(this).attr('number')
if (number.indexOf("P") > -1) {
$(this).attr('label', "");
}
else {
$(this).attr('label', number);
}
} else {
$(this).attr('label', this.textContent.trim().replace('*Page ', ''));
}
});
flooie marked this conversation as resolved.
Show resolved Hide resolved

// Systematize page numbers
$('page-number').each(function (index, element) {
// Get the label and citation index from the current element
const label = $(this).attr('label');
const citationIndex = $(this).attr('citation-index');

// Clean up the label (remove '*') and use it for the new href and id
const cleanLabel = label.replace('*', '').trim();

// Create the new <a> element
const $newAnchor = $('<a></a>')
.addClass('page-label')
.attr('data-citation-index', citationIndex)
.attr('data-label', cleanLabel)
.attr('href', '#' + cleanLabel)
.attr('id', cleanLabel)
.text('*' + cleanLabel);

// Replace the <page-number> element with the new <a> element
$(this).replaceWith($newAnchor);
});

// Systematize page numbers
$('span.star-pagination').each(function (index, element) {
// Get the label and citation index from the current element
const label = $(this).attr('label');
const citationIndex = $(this).attr('citation-index');

// Clean up the label (remove '*') and use it for the new href and id
const cleanLabel = label.replace('*', '').trim();

// Create the new <a> element
const $newAnchor = $('<a></a>')
.addClass('page-label')
.attr('data-citation-index', citationIndex)
.attr('data-label', cleanLabel)
.attr('href', '#' + cleanLabel)
.attr('id', cleanLabel)
.text('*' + cleanLabel);

// Replace the <span> element with the new <a> element
$(this).replaceWith($newAnchor);
});
// Fix weird data-ref bug
document.querySelectorAll('strong').forEach((el) => {
if (/\[\d+\]/.test(el.textContent)) {
// Check if the text matches the pattern [XXX]
const match = el.textContent.match(/\[\d+\]/)[0]; // Get the matched pattern
el.setAttribute('data-ref', match); // Set a data-ref attribute
}
});

///////////////
// Footnotes //
///////////////


// We formatted the harvard footnotes oddly when they appeared inside the pre-opinion content.
// this removes the excess a tags and allows us to standardize footnotes across our contents
// footnote cleanup in harvard
// Update and modify footnotes to enable linking
$('div.footnote > a').remove();
const headfootnotemarks = $('a.footnote');
const divfootnotes = $('div.footnote');

if (headfootnotemarks.length === divfootnotes.length) {
headfootnotemarks.each(function (index) {
const footnoteMark = $(this);
const footnote = divfootnotes.eq(index);

const $newElement = $('<footnotemark></footnotemark>');
$.each(footnoteMark.attributes, function () {
if (footnoteMark.specified) {
$newElement.attr(footnoteMark.name, footnoteMark.value);
}
});
$newElement.html(footnoteMark.html());
footnoteMark.replaceWith($newElement);

const $newFootnote = $('<footnote></footnote>');
$.each(footnote.attributes, function () {
if (footnote.specified) {
$newFootnote.attr(footnote.name, footnote.value);
}
});
$newFootnote.attr('label', footnote.attr('label'));
$newFootnote.html(footnote.html());
footnote.replaceWith($newFootnote);
});
}

// This fixes many of the harvard footnotes so that they can
// easily link back and forth - we have a second set
// of harvard footnotes inside headnotes that need to be parsed out now
// okay.

const footnoteMarks = $('footnotemark');
const footnotes = $('footnote').not('[orphan="true"]');

if (footnoteMarks.length === footnotes.length) {
// we can make this work
footnoteMarks.each(function (index) {
const footnoteMark = $(this);
console.log(index, footnoteMark);
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this log might be a leftover from debugging. Would it be okay to remove it to reduce console clutter?

const $newElement = $('<a></a>');
// Copy attributes from the old element
$.each(footnoteMark.attributes, function () {
if (footnoteMark.specified) {
$newElement.attr(footnoteMark.name, footnoteMark.value);
console.log(footnoteMark.name, footnoteMark.value);
}
});
$newElement.html(footnoteMark.html());
const $supElement = $('<sup></sup>').append($newElement);
footnoteMark.replaceWith($supElement);
const footnote = footnotes.eq(index);
$newElement.attr('href', `#fn${index}`);
$newElement.attr('id', `fnref${index}`);
footnote.attr('id', `fn${index}`);
console.log(footnoteMark, footnote);
flooie marked this conversation as resolved.
Show resolved Hide resolved

const $jumpback = $('<a class="jumpback">↵</a>');
$jumpback.attr('href', `#fnref${index}`);

footnote.append($jumpback);
});
} else {
// If the number of footnotes and footnotemarks are inconsistent use the method to scroll to the nearest one
// we dont use this by default because many older opinions will reuse * ^ and other icons repeatedly on every page
// and so label is no usable to identify the correct footnote.

footnotes.each(function (index) {
console.log($(this));

const $jumpback = $('<a class="jumpback">↵</a>');
$jumpback.attr('label', $(this).attr('label'));
$(this).append($jumpback);
});

// There is no silver bullet for footnotes
$('footnotemark').on('click', function () {
const markText = $(this).text().trim(); // Get the text of the clicked footnotemark
const currentScrollPosition = $(window).scrollTop(); // Get the current scroll position

// Find the first matching footnote below the current scroll position
const targetFootnote = $('footnote')
.filter(function () {
return $(this).attr('label') === markText && $(this).offset().top > currentScrollPosition;
})
.first();

// If a matching footnote is found, scroll to it
if (targetFootnote.length > 0) {
$('html, body').animate(
{
scrollTop: targetFootnote.offset().top,
},
500
); // Adjust the animation duration as needed
} else {
console.warn('No matching footnote found below the current position for:', markText);
}
});


//////////////
// Sidebar //
/////////////

$('.jumpback').on('click', function () {
const footnoteLabel = $(this).attr('label').trim(); // Get the label attribute of the clicked footnote
const currentScrollPosition = $(window).scrollTop(); // Get the current scroll position

// Find the first matching footnotemark above the current scroll position
const targetFootnotemark = $('footnotemark')
.filter(function () {
return $(this).text().trim() === footnoteLabel && $(this).offset().top < currentScrollPosition;
})
.last();

// If a matching footnotemark is found, scroll to it
if (targetFootnotemark.length > 0) {
$('html, body').animate(
{
scrollTop: targetFootnotemark.offset().top,
},
500
); // Adjust the animation duration as needed
} else {
console.warn('No matching footnotemark found above the current position for label:', footnoteLabel);
}
});
}

$(document).ready(function () {
function adjustSidebarHeight() {
if ($(window).width() > 767) {
// Only apply the height adjustment for screens wider than 767px
var scrollTop = $(window).scrollTop();
if (scrollTop <= 175) {
$('.opinion-sidebar').css('height', 'calc(100vh - ' + (175 - scrollTop) + 'px)');
// $('.main-document').css('height', 'calc(100vh + ' + (scrollTop) + 'px)');
} else {
$('.opinion-sidebar').css('height', '100vh');
}
} else {
$('.opinion-sidebar').css('height', 'auto'); // Reset height for mobile view
}
}

// Adjust height on document ready and when window is scrolled or resized
adjustSidebarHeight();
$(window).on('scroll resize', adjustSidebarHeight);
});

// Update sidebar to show where we are on the page
document.addEventListener('scroll', function () {
let sections = document.querySelectorAll('.jump-link');
let links = document.querySelectorAll('.jump-links > a');
let currentSection = '';

// Determine which section is currently in view
sections.forEach((section) => {
let sectionTop = section.offsetTop;
let sectionHeight = section.offsetHeight;
if (window.scrollY >= sectionTop - sectionHeight / 3) {
currentSection = section.getAttribute('id');
}
});

// Remove the active class from all links and their parent elements
links.forEach((link) => {
link.classList.remove('active');
if (link.parentElement) {
link.parentElement.classList.remove('active');
}
});

// Add the active class to the link and its parent that corresponds to the current section
links.forEach((link) => {
if (link.getAttribute('href') === `#${currentSection}`) {
link.classList.add('active');
if (link.parentElement) {
link.parentElement.classList.add('active');
}
}
});
});
Comment on lines +240 to +271
Copy link
Contributor

Choose a reason for hiding this comment

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

We can optimize this event listener by implementing a few tweaks. Here's a breakdown:

  1. Reduce Iterations: Instead of looping through all nav links twice, we can initially query only the links with the active class. This significantly reduces the number of elements to process in the first loop.

  2. Use Unique IDs: To streamline the process further, we can assign unique IDs to the target anchors. This allows us to directly target the specific element using getElementById, eliminating the need for a second loop and href attribute comparisons.

Here's my suggestion for this event listener:

Suggested change
document.addEventListener('scroll', function () {
let sections = document.querySelectorAll('.jump-link');
let links = document.querySelectorAll('.jump-links > a');
let currentSection = '';
// Determine which section is currently in view
sections.forEach((section) => {
let sectionTop = section.offsetTop;
let sectionHeight = section.offsetHeight;
if (window.scrollY >= sectionTop - sectionHeight / 3) {
currentSection = section.getAttribute('id');
}
});
// Remove the active class from all links and their parent elements
links.forEach((link) => {
link.classList.remove('active');
if (link.parentElement) {
link.parentElement.classList.remove('active');
}
});
// Add the active class to the link and its parent that corresponds to the current section
links.forEach((link) => {
if (link.getAttribute('href') === `#${currentSection}`) {
link.classList.add('active');
if (link.parentElement) {
link.parentElement.classList.add('active');
}
}
});
});
document.addEventListener('scroll', function () {
let sections = document.querySelectorAll('.jump-link');
let currentSection = '';
// Determine which section is currently in view
sections.forEach((section) => {
let sectionTop = section.offsetTop;
let sectionHeight = section.offsetHeight;
if (window.scrollY >= sectionTop - sectionHeight / 3) {
currentSection = section.getAttribute('id');
}
});
if (!currentSection) currentSection = 'top';
// Remove the active class from links and their parent elements
let links = document.querySelectorAll('.jump-links > a.active');
links.forEach((link) => {
link.classList.remove('active');
if (link.parentElement) {
link.parentElement.classList.remove('active');
}
});
// Add the active class to the link and its parent that corresponds to the current section
let activeLink = document.getElementById(`nav_${currentSection}`);
if (!activeLink) return;
activeLink.classList.add('active');
if (activeLink.parentElement) {
activeLink.parentElement.classList.add('active');
}
});

Beyond the script adjustments, we'll need to add IDs to the opinions.html template. To make this refactor easier to review, I've created a new branch. You can check the diff here:

https://github.com/freelawproject/courtlistener/compare/ordering-front-end-redesign...ERosendo:courtlistener:refactor-scroll-event-lister?expand=1

3 changes: 3 additions & 0 deletions cl/favorites/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils.timezone import now
from selenium.webdriver.common.by import By
from timeout_decorator import timeout_decorator
from waffle.testutils import override_flag

from cl.favorites.factories import NoteFactory, PrayerFactory
from cl.favorites.models import DocketTag, Note, Prayer, UserTag
Expand Down Expand Up @@ -96,6 +97,7 @@ def setUp(self) -> None:
super().setUp()

@timeout_decorator.timeout(SELENIUM_TIMEOUT)
@override_flag("ui_flag_for_o", False)
def test_anonymous_user_is_prompted_when_favoriting_an_opinion(
self,
) -> None:
Expand Down Expand Up @@ -156,6 +158,7 @@ def test_anonymous_user_is_prompted_when_favoriting_an_opinion(
modal_title = self.browser.find_element(By.ID, "save-note-title")
self.assertIn("Save Note", modal_title.text)

@override_flag("ui_flag_for_o", False)
@timeout_decorator.timeout(SELENIUM_TIMEOUT)
def test_logged_in_user_can_save_note(self) -> None:
# Meta: assure no Faves even if part of fixtures
Expand Down
Loading
Loading