Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "outline-browser",
"version": "0.0.2",
"version": "0.0.3",
"scripts": {
"dev": "npx serve public",
"webpack-local": "ENVIRONMENT=development npx webpack -w",
Expand Down
234 changes: 220 additions & 14 deletions public/assets/bundle.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions public/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ code {
color: var(--color-hidden-cursor-text);
}

.task-checkbox {
float: left;
margin-right: 0.5rem;
margin-top: 0.4rem;
}

/* Theme Selector Modal */
.theme-selector {
min-height: 200px;
Expand Down
36 changes: 19 additions & 17 deletions src/keyboard-shortcuts/all.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import {KeyEventDefinition} from "./base";
import { KeyEventDefinition } from "./base";
import { sidebarToggle } from "./sidebar-toggle";
import { j } from './j';
import {k} from "./k";
import {l} from './l';
import {h} from "./h";
import {z} from "./z";
import {$} from "./$";
import {i} from "./i";
import {archive} from "./archive";
import {tab} from "./tab";
import {enter} from "./enter";
import {d} from "./delete";
import {lift} from "./lift";
import {lower} from "./lower";
import {swapUp} from "./swap-up";
import {swapDown} from "./swap-down";
import {escapeEditing} from "./escape-editing";
import {themeSelector} from "./theme-selector";
import { k } from "./k";
import { l } from './l';
import { h } from "./h";
import { z } from "./z";
import { $ } from "./$";
import { i } from "./i";
import { archive } from "./archive";
import { tab } from "./tab";
import { enter } from "./enter";
import { d } from "./delete";
import { lift } from "./lift";
import { lower } from "./lower";
import { swapUp } from "./swap-up";
import { swapDown } from "./swap-down";
import { escapeEditing } from "./escape-editing";
import { themeSelector } from "./theme-selector";
import { t } from './t';

export const AllShortcuts: KeyEventDefinition[] = [
sidebarToggle,
Expand All @@ -26,6 +27,7 @@ export const AllShortcuts: KeyEventDefinition[] = [
l,
h,
z,
t,
$,
i,
archive,
Expand Down
73 changes: 69 additions & 4 deletions src/keyboard-shortcuts/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,80 @@ import { KeyEventDefinition } from "./base";

export const archive: KeyEventDefinition = {
context: 'navigation',
keys: ['shift + x'],
description: 'Mark a node and all its children as "archived"',
keys: ['shift + x', 'ctrl + x'],
description: 'Mark a node as archived; ctrl+x also completes task',
action: async args => {
const { e, outline, cursor, api } = args;
e.preventDefault();
// toggle "strikethrough" of node
cursor.get().classList.toggle('strikethrough');
outline.getContentNode(cursor.getIdOfNode()).toggleArchiveStatus();
api.saveContentNode(outline.getContentNode(cursor.getIdOfNode()));
const node = outline.getContentNode(cursor.getIdOfNode());
node.toggleArchiveStatus();

if (node.task) {
if (e.ctrlKey || node.archived) {
node.markComplete();
}
else {
node.markIncomplete();
}
}

// re-render content to reflect completion checkbox for the currently focused node (tasks aggregate)
const contentEl = cursor.get().querySelector('.nodeContent') as HTMLElement;
contentEl.innerHTML = await outline.renderContent(cursor.getIdOfNode());

// Also update the original outline node's content and strikethrough state
const nodeId = cursor.getIdOfNode();
const originalNodes = Array.from(document.querySelectorAll(`.node[data-id="${nodeId}"]`)) as HTMLElement[];
const original = originalNodes.find(n => !n.closest('#id-tasks-aggregate'));
if (original) {
const originalContentEl = original.querySelector('.nodeContent') as HTMLElement;
if (originalContentEl) {
originalContentEl.innerHTML = await outline.renderContent(nodeId);
}
const isCompletedTask = !!node.completionDate;
if (node.isArchived() || isCompletedTask) {
original.classList.add('strikethrough');
}
else {
original.classList.remove('strikethrough');
}
}

// Keep tasklist in sync for incremental updates without full render
if (node.task) {
outline.tasklist[nodeId] = node;
}
else {
delete outline.tasklist[nodeId];
}

// Refresh Tasks aggregate at the top
const tasksHtml = await outline.renderTasksFromTasklist();
const tasksContainer = document.getElementById('id-tasks-aggregate');
if (tasksHtml.length === 0) {
if (tasksContainer) {
tasksContainer.remove();
}
}
else {
if (tasksContainer) {
tasksContainer.outerHTML = tasksHtml;
}
else {
const root = document.querySelector('#outliner');
if (root) {
root.insertAdjacentHTML('afterbegin', tasksHtml);
}
}
}

// Keep cursor where the user was interacting: tasks aggregate vs main outline
const inTasksAggregate = !!cursor.get()?.closest('#id-tasks-aggregate');
cursor.set(inTasksAggregate ? `#tasks-id-${nodeId}` : `#id-${nodeId}`);

api.saveContentNode(node);
api.save(outline);

}
Expand Down
8 changes: 6 additions & 2 deletions src/keyboard-shortcuts/j.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ export const j: KeyEventDefinition = {
action: async args => {
// move cursor down
// if shift key is held, swap the node with its next sibling
const sibling = args.cursor.get().nextElementSibling;
const el = args.cursor.get();
const sibling = el.nextElementSibling as HTMLElement | null;

if (sibling) {
if (!args.e.shiftKey) {
args.cursor.set(`#id-${sibling.getAttribute('data-id')}`);
const isTasksContainer = el.id === 'id-tasks-aggregate';
const inTasksAggregate = !!el.closest('#id-tasks-aggregate') && !isTasksContainer;
const prefix = inTasksAggregate ? '#tasks-id-' : '#id-';
args.cursor.set(`${prefix}${sibling.getAttribute('data-id')}`);
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/keyboard-shortcuts/k.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ export const k: KeyEventDefinition = {
const { cursor, e } = args;
// move cursor up
// if shift key is held, swap the node with its previous sibling
const sibling = cursor.get().previousElementSibling;
const el = cursor.get();
const sibling = el.previousElementSibling as HTMLElement | null;

if (sibling && !sibling.classList.contains('nodeContent')) {
if (!e.shiftKey) {
cursor.set(`#id-${sibling.getAttribute('data-id')}`);
const isTasksContainer = el.id === 'id-tasks-aggregate';
const inTasksAggregate = !!el.closest('#id-tasks-aggregate') && !isTasksContainer;
const prefix = inTasksAggregate ? '#tasks-id-' : '#id-';
cursor.set(`${prefix}${sibling.getAttribute('data-id')}`);
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/keyboard-shortcuts/l.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ export const l: KeyEventDefinition = {
if (cursor.isNodeCollapsed()) {
return;
}
const children = cursor.get().querySelector('.node');
const el = cursor.get();
const children = el.querySelector('.node') as HTMLElement | null;
if (children) {
cursor.set(`#id-${children.getAttribute('data-id')}`);
const isTasksContainer = el.id === 'id-tasks-aggregate';
const inTasksAggregate = !!el.closest('#id-tasks-aggregate') && !isTasksContainer;
const prefix = inTasksAggregate || isTasksContainer ? '#tasks-id-' : '#id-';
cursor.set(`${prefix}${children.getAttribute('data-id')}`);
}
}
}
Expand Down
72 changes: 72 additions & 0 deletions src/keyboard-shortcuts/t.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { KeyEventDefinition } from './base';

export const t: KeyEventDefinition = {
context: 'navigation',
keys: ['t'],
description: 'Toggle current node as a task',
action: async args => {
const { e, cursor, outline, api } = args;
e.preventDefault();

const nodeId = cursor.getIdOfNode();
const node = outline.getContentNode(nodeId);
node.toggleTask();

// Re-render just the content for this node to show/hide checkbox
const contentEl = cursor.get().querySelector('.nodeContent') as HTMLElement;
contentEl.innerHTML = await outline.renderContent(nodeId);

// Also update the original outline node's content and strikethrough state
const originalNodes = Array.from(document.querySelectorAll(`.node[data-id="${nodeId}"]`)) as HTMLElement[];
const original = originalNodes.find(n => !n.closest('#id-tasks-aggregate'));
if (original) {
const originalContentEl = original.querySelector('.nodeContent') as HTMLElement;
if (originalContentEl) {
originalContentEl.innerHTML = await outline.renderContent(nodeId);
}
const isCompletedTask = !!node.completionDate;
if (node.isArchived() || isCompletedTask) {
original.classList.add('strikethrough');
}
else {
original.classList.remove('strikethrough');
}
}

// Keep tasklist in sync for incremental updates without full render
if (node.task) {
outline.tasklist[nodeId] = node;
}
else {
delete outline.tasklist[nodeId];
}

// Refresh Tasks aggregate at the top
const tasksHtml = await outline.renderTasksFromTasklist();
const tasksContainer = document.getElementById('id-tasks-aggregate');
if (tasksHtml.length === 0) {
if (tasksContainer) {
tasksContainer.remove();
}
}
else {
if (tasksContainer) {
tasksContainer.outerHTML = tasksHtml;
}
else {
const root = document.querySelector('#outliner');
if (root) {
root.insertAdjacentHTML('afterbegin', tasksHtml);
}
}
}

// Keep cursor where the user was interacting: tasks aggregate vs main outline
const inTasksAggregate = !!cursor.get()?.closest('#id-tasks-aggregate');
cursor.set(inTasksAggregate ? `#tasks-id-${nodeId}` : `#id-${nodeId}`);

api.saveContentNode(node);
api.save(outline);
}
}

35 changes: 34 additions & 1 deletion src/lib/contentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface IContentNode {
archiveDate?: number;
deleted: boolean;
deletedDate?: number;
task?: boolean;
completionDate?: number;
}

export class ContentNode implements IContentNode {
Expand All @@ -20,6 +22,8 @@ export class ContentNode implements IContentNode {
archiveDate?: number;
deleted: boolean;
deletedDate?: number;
task?: boolean;
completionDate?: number;

constructor(id?: string, content?: string) {
this.id = id;
Expand All @@ -29,6 +33,9 @@ export class ContentNode implements IContentNode {

this.archived = false;
this.deleted = false;

this.task = false;
this.completionDate = null;
}

static Create(data: IContentNode): ContentNode {
Expand All @@ -42,6 +49,10 @@ export class ContentNode implements IContentNode {
node.deleted = data.deleted;
node.deletedDate = data.deletedDate;

// Backwards compatibility with saved data that may not have these fields
node.task = (data as any).task ?? false;
node.completionDate = (data as any).completionDate ?? null;

return node;
}

Expand Down Expand Up @@ -87,6 +98,26 @@ export class ContentNode implements IContentNode {
this.deletedDate = Date.now();
}

// Task helpers
toggleTask() {
if (this.task) {
this.task = false;
this.completionDate = null;
}
else {
this.task = true;
}
}

markComplete() {
this.task = true;
this.completionDate = Date.now();
}

markIncomplete() {
this.completionDate = null;
}

toJson(): IContentNode {
return {
id: this.id,
Expand All @@ -97,7 +128,9 @@ export class ContentNode implements IContentNode {
archived: this.archived,
archiveDate: this.archiveDate,
deleted: this.deleted,
deletedDate: this.deletedDate
deletedDate: this.deletedDate,
task: this.task,
completionDate: this.completionDate
};
}

Expand Down
Loading
Loading