Highlight viewed section within the list (#76)

* Add fallback primary fonts

* Highlight viewed section in the navbar

* Fix moving cells with empty sections

* Reword attribute
This commit is contained in:
Jonatan Kłosko 2021-03-12 16:40:37 +01:00 committed by GitHub
parent a2d1e2f934
commit 7fa2b44666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 116 additions and 21 deletions

View file

@ -5,5 +5,6 @@ button:focus {
}
body {
font-family: "Inter";
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}

View file

@ -38,3 +38,7 @@ and entering/escaping insert mode.
[data-element="cell"]:not([data-js-focused]) [data-element="actions"] {
@apply hidden;
}
[data-element="section-list-item"][data-js-is-viewed] {
@apply text-current;
}

View file

@ -1,5 +1,5 @@
import "../css/app.css";
import 'remixicon/fonts/remixicon.css'
import "remixicon/fonts/remixicon.css";
import "@fontsource/inter";
import "@fontsource/inter/500.css";

View file

@ -27,7 +27,9 @@ const Cell = {
this.pushEvent("cell_init", { cell_id: this.props.cellId }, (payload) => {
const { source, revision } = payload;
const editorContainer = this.el.querySelector(`[data-element="editor-container"]`);
const editorContainer = this.el.querySelector(
`[data-element="editor-container"]`
);
// Remove the content placeholder.
editorContainer.firstElementChild.remove();
// Create an empty container for the editor to be mounted in.

View file

@ -77,7 +77,7 @@ class LiveEditor {
occurrencesHighlight: false,
renderLineHighlight: "none",
theme: "custom",
fontFamily: "JetBrains Mono"
fontFamily: "JetBrains Mono",
});
this.editor.getModel().updateOptions({

View file

@ -37,10 +37,16 @@ const Session = {
document.addEventListener("mousedown", this.handleDocumentMouseDown);
this.el.querySelector(`[data-element="section-list"]`).addEventListener("click", event => {
getSectionList().addEventListener("click", (event) => {
handleSectionListClick(this, event);
});
updateSectionListHighlight();
getNotebook().addEventListener("scroll", (event) => {
updateSectionListHighlight()
});
// Server events
this.handleEvent("cell_inserted", ({ cell_id: cellId }) => {
@ -178,7 +184,9 @@ function handleDocumentMouseDown(hook, event) {
// Depending on whether the click targets editor disable/enable insert mode
if (cell) {
const editorContainer = cell.querySelector(`[data-element="editor-container"]`);
const editorContainer = cell.querySelector(
`[data-element="editor-container"]`
);
const editorClicked = editorContainer.contains(event.target);
const insertMode = editorClicked;
if (hook.state.insertMode !== insertMode) {
@ -191,7 +199,9 @@ function handleDocumentMouseDown(hook, event) {
* Handles section link clicks in the section list.
*/
function handleSectionListClick(hook, event) {
const sectionButton = event.target.closest(`[data-element="section-list-item"]`);
const sectionButton = event.target.closest(
`[data-element="section-list-item"]`
);
if (sectionButton) {
const sectionId = sectionButton.getAttribute("data-section-id");
const section = getSectionById(sectionId);
@ -199,6 +209,36 @@ function handleSectionListClick(hook, event) {
}
}
/**
* Handles the main notebook area being scrolled.
*/
function updateSectionListHighlight() {
const currentListItem = document.querySelector(
`[data-element="section-list-item"][data-js-is-viewed]`
);
if (currentListItem) {
currentListItem.removeAttribute("data-js-is-viewed");
}
// Consider a section being viewed if it is within the top 35% of the screen
const viewedSection = getSections()
.reverse()
.find((section) => {
const { top } = section.getBoundingClientRect();
const scrollTop = document.documentElement.scrollTop;
return top <= scrollTop + window.innerHeight * 0.35;
});
if (viewedSection) {
const sectionId = viewedSection.getAttribute("data-section-id");
const listItem = document.querySelector(
`[data-element="section-list-item"][data-section-id="${sectionId}"]`
);
listItem.setAttribute("data-js-is-viewed", "true");
}
}
// User action handlers (mostly keybindings)
function deleteFocusedCell(hook) {
@ -376,7 +416,7 @@ function handleCellMoved(hook, cellId) {
function handleSectionInserted(hook, sectionId) {
const section = getSectionById(sectionId);
const nameElement = section.querySelector("[data-section-name]");
const nameElement = section.querySelector(`[data-element="section-name"]`);
nameElement.focus({ preventScroll: true });
selectElementContent(nameElement);
smoothlyScrollToElement(nameElement);
@ -445,6 +485,14 @@ function getSectionById(sectionId) {
);
}
function getSectionList() {
return document.querySelector(`[data-element="section-list"]`);
}
function getNotebook() {
return document.querySelector(`[data-element="notebook"]`);
}
function cancelEvent(event) {
// Cancel any default browser behavior.
event.preventDefault();

View file

@ -6,9 +6,9 @@ describe("PubSub", () => {
const callback1 = jest.fn();
const callback2 = jest.fn();
pubsub.subscribe('topic1', callback1);
pubsub.subscribe('topic2', callback2);
pubsub.broadcast('topic1', { data: 1 });
pubsub.subscribe("topic1", callback1);
pubsub.subscribe("topic2", callback2);
pubsub.broadcast("topic1", { data: 1 });
expect(callback1).toHaveBeenCalledWith({ data: 1 });
expect(callback2).not.toHaveBeenCalled();
@ -18,9 +18,9 @@ describe("PubSub", () => {
const pubsub = new PubSub();
const callback1 = jest.fn();
pubsub.subscribe('topic1', callback1);
pubsub.unsubscribe('topic1', callback1);
pubsub.broadcast('topic1', {});
pubsub.subscribe("topic1", callback1);
pubsub.unsubscribe("topic1", callback1);
pubsub.broadcast("topic1", {});
expect(callback1).not.toHaveBeenCalled();
});

View file

@ -191,10 +191,7 @@ defmodule Livebook.Notebook do
{cell, separated_cells} = List.pop_at(separated_cells, idx)
separated_cells = List.insert_at(separated_cells, new_idx, cell)
cell_groups =
separated_cells
|> Enum.chunk_by(&(&1 == :separator))
|> Enum.reject(&(&1 == [:separator]))
cell_groups = group_cells(separated_cells)
sections =
notebook.sections
@ -204,6 +201,30 @@ defmodule Livebook.Notebook do
%{notebook | sections: sections}
end
defp group_cells(separated_cells) do
separated_cells
|> Enum.reverse()
|> do_group_cells([])
end
defp do_group_cells([], groups), do: groups
defp do_group_cells([:separator | separated_cells], []) do
do_group_cells(separated_cells, [[], []])
end
defp do_group_cells([:separator | separated_cells], groups) do
do_group_cells(separated_cells, [[] | groups])
end
defp do_group_cells([cell | separated_cells], []) do
do_group_cells(separated_cells, [[cell]])
end
defp do_group_cells([cell | separated_cells], [group | groups]) do
do_group_cells(separated_cells, [[cell | group] | groups])
end
defp clamp_index(index, list) do
index |> max(0) |> min(length(list) - 1)
end

View file

@ -7,7 +7,7 @@ defmodule LivebookWeb.SectionComponent do
<div class="flex space-x-4 items-center">
<div class="flex flex-grow space-x-2 items-center text-gray-600">
<h2 class="flex-grow text-gray-900 font-semibold text-3xl py-2 border-b-2 border-transparent hover:border-blue-100 focus:border-blue-300"
data-section-name
data-element="section-name"
id="section-<%= @section.id %>-name"
contenteditable
spellcheck="false"

View file

@ -130,7 +130,7 @@ defmodule LivebookWeb.SessionLive do
<% end %>
</div>
</div>
<div class="flex-grow px-6 py-8 flex overflow-y-auto">
<div class="flex-grow px-6 py-8 flex overflow-y-auto" data-element="notebook">
<div class="max-w-screen-lg w-full mx-auto">
<div class="mb-8">
<h1 class="text-gray-900 font-semibold text-4xl pb-2 border-b-2 border-transparent hover:border-blue-100 focus:border-blue-300"
@ -141,7 +141,7 @@ defmodule LivebookWeb.SessionLive do
phx-hook="ContentEditable"
data-update-attribute="phx-value-name"><%= @data.notebook.name %></h1>
</div>
<div class="flex flex-col space-y-16 pb-80">
<div class="flex flex-col space-y-16">
<%= for section <- @data.notebook.sections do %>
<%= live_component @socket, LivebookWeb.SectionComponent,
id: section.id,
@ -149,6 +149,7 @@ defmodule LivebookWeb.SessionLive do
section: section,
cell_infos: @data.cell_infos %>
<% end %>
<div style="height: 80vh"></div>
</div>
</div>
</div>

View file

@ -45,4 +45,22 @@ defmodule Livebook.NotebookTest do
assert :error == Notebook.fetch_cell_sibling(notebook, cell2.id, 2)
end
end
describe "move_cell/3" do
test "preserves empty sections" do
cell1 = %{Cell.new(:markdown) | id: "1"}
notebook = %{
Notebook.new()
| sections: [
%{Section.new() | cells: [cell1]},
%{Section.new() | cells: []}
]
}
new_notebook = Notebook.move_cell(notebook, cell1.id, 1)
assert %{sections: [%{cells: []}, %{cells: [^cell1]}]} = new_notebook
end
end
end