Fix note-field space bar hijacking video, rework Outline topics as documents

- Lesson page's global keyboard shortcuts (space/arrows for play-pause/
  seek/volume) fired regardless of focus, so typing a space in the notes
  textarea toggled the video instead of typing. Now skipped entirely
  whenever a text field/select has focus.
- Outline topics no longer spawn their own top-level collection each time.
  A topic is now a document inside one fixed, user-configured collection
  (Settings -> Outline Integration -> Default collection), and a lesson's
  note becomes a child document nested under its topic - matching how
  notes actually get organized in a real Outline instance instead of
  cluttering the collections list with one per topic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 12:27:00 -04:00
co-authored by Claude Sonnet 5
parent 5fe3fafc7a
commit 145f91a647
4 changed files with 168 additions and 36 deletions
+13 -4
View File
@@ -606,11 +606,11 @@
const topicStatus = document.getElementById('outline-topic-status');
if (topicSelect) {
fetch('/api/outline/collections')
fetch('/api/outline/topics')
.then(r => r.json())
.then(data => {
const newOption = topicSelect.querySelector('option[value="__new__"]');
(data.collections || []).forEach(function(c) {
(data.topics || []).forEach(function(c) {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name;
@@ -624,7 +624,7 @@
newTopicInput.value = INITIAL_TOPIC_NAME;
}
if (data.error && topicStatus) {
topicStatus.textContent = 'Outline not reachable - topic list may be incomplete';
topicStatus.textContent = data.error;
}
})
.catch(() => {});
@@ -805,8 +805,17 @@
}, 3000);
}
// Keyboard shortcuts
// Keyboard shortcuts - skip while typing in the note (or any
// other text field), so space/arrow keys type normally instead
// of hijacking video playback.
function isTypingTarget(el) {
if (!el) return false;
const tag = el.tagName;
return tag === 'TEXTAREA' || tag === 'INPUT' || tag === 'SELECT' || el.isContentEditable;
}
document.addEventListener('keydown', function(e) {
if (isTypingTarget(e.target)) return;
if (activeMedia && !e.ctrlKey && !e.altKey && !e.metaKey) {
switch(e.key) {
case ' ':
+37 -3
View File
@@ -524,8 +524,9 @@
<div class="card">
<h2>Outline Integration</h2>
<p class="setting-desc" style="margin-bottom: 12px;">
Pick a topic on a lesson's note (on the lesson page) to push it to that topic's collection in
Outline when you leave the page. Notes without a topic stay local only.
Pick a topic on a lesson's note (on the lesson page) to push it there when you leave the page -
each topic becomes a document inside the collection below, and the note is nested under it.
Notes without a topic stay local only.
</p>
<div class="setting-row" style="flex-direction: column; align-items: stretch; gap: 8px;">
<label for="outline-url">Outline URL
@@ -536,6 +537,12 @@
<label for="outline-token" style="margin-top: 4px;">API token</label>
<input type="password" id="outline-token" class="hex-input" style="width: 100%; font-family: var(--font-family);"
placeholder="Leave blank to keep the current token">
<label for="outline-collection" style="margin-top: 4px;">Default collection
<span class="setting-desc">Where topic documents get created - e.g. "Training Notes"</span>
</label>
<select id="outline-collection">
<option value="">Save your URL/token first, then pick one</option>
</select>
<div style="display: flex; gap: 10px; margin-top: 4px;">
<button class="btn" onclick="saveOutlineConfig()">Save</button>
<button class="btn btn-secondary" onclick="testOutlineConnection()">Test Connection</button>
@@ -1002,6 +1009,25 @@
document.getElementById('outline-url').value = data.base_url || '';
document.getElementById('outline-config-status').textContent =
data.configured ? 'Configured' : 'Not configured';
loadOutlineCollectionsDropdown(data.default_collection_id || '');
})
.catch(() => {});
}
function loadOutlineCollectionsDropdown(selectedId) {
const select = document.getElementById('outline-collection');
fetch('/api/outline/collections')
.then(r => r.json())
.then(data => {
const collections = data.collections || [];
if (collections.length === 0) {
select.innerHTML = '<option value="">No collections found - check your URL/token</option>';
return;
}
select.innerHTML = collections.map(c =>
`<option value="${c.id}">${c.name}</option>`
).join('');
if (selectedId) select.value = selectedId;
})
.catch(() => {});
}
@@ -1010,12 +1036,20 @@
const status = document.getElementById('outline-test-status');
const url = document.getElementById('outline-url').value.trim();
const token = document.getElementById('outline-token').value.trim();
const collectionSelect = document.getElementById('outline-collection');
const collectionId = collectionSelect.value;
const collectionName = collectionId ? collectionSelect.options[collectionSelect.selectedIndex].text : '';
status.style.color = 'var(--text-muted)';
status.textContent = 'Saving...';
fetch('/api/outline/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ base_url: url, api_token: token })
body: JSON.stringify({
base_url: url,
api_token: token,
default_collection_id: collectionId,
default_collection_name: collectionName
})
})
.then(r => r.json())
.then(data => {