Skip to content

Commit

Permalink
Add context menu to tables
Browse files Browse the repository at this point in the history
- hold Ctrl (or Cmd), and right-click anywhere in the table for context menu to show
- context menu allows to align the table (Left/Center/Right)
- create <TableContextMenu>
- create reusable <ContextMenu> (used by <NoteContextMenu> and <TableContextMenu>)
- add #context-menu-container, and make it easier to remove context menus
- fix init tables on input (couldn't resize new table wo opening note again)
- simplify how "NoteEdited" (before "editnote") event is dispatched
  • Loading branch information
penge committed Dec 20, 2023
1 parent 1cfa45a commit 464e44c
Show file tree
Hide file tree
Showing 17 changed files with 276 additions and 117 deletions.
56 changes: 49 additions & 7 deletions public/notes.css
Original file line number Diff line number Diff line change
Expand Up @@ -328,9 +328,6 @@ body.resizing-sidebar { cursor: col-resize; }
body.resizing-sidebar-locked-min { cursor: e-resize; }
body.resizing-sidebar-locked-max { cursor: w-resize; }

body.resizing-table-column { cursor: col-resize; }
body.resizing-table-row { cursor: row-resize; }

/* Note */

#content-container {
Expand Down Expand Up @@ -395,6 +392,15 @@ body.with-control #content a > * {

/* My Notes classes */

.my-notes-table-align-center {
margin-left: auto;
margin-right: auto;
}

.my-notes-table-align-right {
margin-left: auto;
}

.my-notes-highlight {
background: var(--highlight-background-color, yellow) !important;
color: var(--highlight-text-color, black) !important;
Expand Down Expand Up @@ -450,7 +456,7 @@ body.with-control #content a > * {
}

.table-resizing-div.active {
background: var(--table-resizing-line-color);
background: var(--resizing-line-color);
}

.table-column-resizing-div {
Expand All @@ -469,6 +475,9 @@ body.with-control #content a > * {
height: 4px;
}

body.resizing-table-column { cursor: col-resize; }
body.resizing-table-row { cursor: row-resize; }

/* Locked */

body.locked #sidebar,
Expand Down Expand Up @@ -565,20 +574,23 @@ body.with-command-palette #toolbar {
background: var(--context-menu-background-color);
color: var(--context-menu-text-color);
cursor: pointer;
}

#context-menu .action:not(.group) {
padding: .8em;
}

#context-menu .action:hover:not(.disabled) {
#context-menu .action:hover:not(.group):not(.disabled) {
background: var(--context-menu-hover-background-color);
color: var(--context-menu-hover-text-color);
}

#context-menu .action:first-child {
#context-menu .action:not(.inline):first-child {
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}

#context-menu .action:last-child {
#context-menu .action:not(.inline):last-child {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
Expand All @@ -588,6 +600,36 @@ body.with-command-palette #toolbar {
color: var(--context-menu-disabled-text-color);
}

#context-menu .action > svg {
height: 1em;
fill: var(--context-menu-text-color);
}

#context-menu .action.group {
display: flex;
}

#context-menu .action.group:first-child > .action:first-child {
border-top-left-radius: 5px;
}

#context-menu .action.group:first-child > .action:last-child {
border-top-right-radius: 5px;
}

#context-menu .action.group:last-child > .action:first-child {
border-bottom-left-radius: 5px;
}

#context-menu .action.group:last-child > .action:last-child {
border-bottom-right-radius: 5px;
}

#context-menu .action.inline {
display: flex;
align-items: center;
}

/* Toolbar */

#toolbar {
Expand Down
2 changes: 1 addition & 1 deletion public/themes/dark.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
--table-border: 3px solid #454545;
--table-td-border: 1px solid #222;
--table-td-heading-background-color: #222;
--table-resizing-line-color: #0000ff;
--resizing-line-color: #0000ff;


/* Command palette */
Expand Down
2 changes: 1 addition & 1 deletion public/themes/light.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
--table-border: 3px solid #171717;
--table-td-border: 1px solid silver;
--table-td-heading-background-color: #dddddd;
--table-resizing-line-color: #0000ff;
--resizing-line-color: #0000ff;


/* Command palette */
Expand Down
59 changes: 38 additions & 21 deletions src/notes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ContentNote } from "notes/components/content/common";
import __CommandPalette, { CommandPaletteProps } from "notes/components/CommandPalette";
import __Toolbar from "notes/components/Toolbar";

import __ContextMenu, { ContextMenuProps } from "notes/components/ContextMenu";
import NoteContextMenu, { NoteContextMenuProps } from "notes/components/NoteContextMenu";
import __RenameNoteModal, { RenameNoteModalProps } from "notes/components/modals/RenameNoteModal";
import __DeleteNoteModal, { DeleteNoteModalProps } from "notes/components/modals/DeleteNoteModal";
import __NewNoteModal, { NewNoteModalProps } from "notes/components/modals/NewNoteModal";
Expand Down Expand Up @@ -102,12 +102,18 @@ const Notes = (): h.JSX.Element | null => {
const [autoSync, setAutoSync] = useState<boolean>(false);

// Modals
const [contextMenuProps, setContextMenuProps] = useState<ContextMenuProps | null>(null);
const [noteContextMenuProps, setNoteContextMenuProps] = useState<NoteContextMenuProps | null>(null);
const [renameNoteModalProps, setRenameNoteModalProps] = useState<RenameNoteModalProps | null>(null);
const [deleteNoteModalProps, setDeleteNoteModalProps] = useState<DeleteNoteModalProps | null>(null);
const [newNoteModalProps, setNewNoteModalProps] = useState<NewNoteModalProps | null>(null);
const [commandPaletteProps, setCommandPaletteProps] = useState<CommandPaletteProps | null>(null);

useEffect(() => {
if (noteContextMenuProps) {
render(<NoteContextMenu {...noteContextMenuProps} />, document.getElementById("context-menu-container")!);
}
}, [noteContextMenuProps]);

useEffect(() => {
chrome.runtime.getPlatformInfo((platformInfo) => setOs(platformInfo.os === "mac" ? "mac" : "other"));
chrome.tabs.getCurrent((currentTab) => currentTab && setTabId(currentTab.id));
Expand Down Expand Up @@ -269,7 +275,7 @@ const Notes = (): h.JSX.Element | null => {
// Update note content if updated from background
const setBy: string | undefined = changes.setBy && changes.setBy.newValue;
if (
(setBy && !setBy.startsWith(`${tabId}-`)) // expecting "worker-*" or "sync-*"
(setBy && !setBy.startsWith(`${tabId}-`)) // can be other tab, "worker-*", or "sync-*"
&& (newActive in oldNotes)
&& (newActive in newNotes)
&& (newNotes[newActive].content !== oldNotes[newActive].content)
Expand Down Expand Up @@ -382,15 +388,29 @@ const Notes = (): h.JSX.Element | null => {
document.body.classList.toggle("focus", focus);
}, [focus]);

// Hide context menu on click anywhere
const removeContextMenus = useCallback(() => {
setNoteContextMenuProps(null);

const container = document.getElementById("context-menu-container");
if (container) {
render("", container);
}

return true;
}, []);

// Remove context menus on click anywhere
useEffect(() => {
document.addEventListener("click", (event) => {
if ((event.target as Element).closest("#context-menu") === null) {
setContextMenuProps(null);
}
document.addEventListener("click", () => {
removeContextMenus();
});
}, []);

// Remove context menus on changed active note
useEffect(() => {
removeContextMenus();
}, [notesProps.active]);

// Activate note
useEffect(() => {
if (!notesProps.active) {
Expand Down Expand Up @@ -425,7 +445,7 @@ const Notes = (): h.JSX.Element | null => {

keyboardShortcuts.register(os);
keyboardShortcuts.subscribe(KeyboardShortcut.OnEscape, () => {
setContextMenuProps(null);
removeContextMenus();
setCommandPaletteProps((prev) => {
if (prev) {
range.restore();
Expand All @@ -443,7 +463,13 @@ const Notes = (): h.JSX.Element | null => {
keyboardShortcuts.subscribe(KeyboardShortcut.OnSync, () => sendMessage(MessageType.SYNC));
}, [os]);

const [setOnControlHandler] = useKeyboardShortcut(KeyboardShortcut.OnControl);

useEffect(() => {
setOnControlHandler(() => {
document.body.classList.add("with-control");
});

window.addEventListener("blur", () => {
document.body.classList.remove("with-control");
});
Expand Down Expand Up @@ -573,11 +599,10 @@ const Notes = (): h.JSX.Element | null => {
active={notesProps.active}
width={sidebarWidth}
onActivateNote={handleOnActivateNote}
onNoteContextMenu={(noteName, x, y) => setContextMenuProps({
onNoteContextMenu={(noteName, x, y) => removeContextMenus() && setNoteContextMenuProps({
x,
y,
onRename: () => {
setContextMenuProps(null);
setRenameNoteModalProps({
noteName,
validate: (newNoteName: string) => newNoteName.length > 0 && newNoteName !== noteName && !(newNoteName in notesProps.notes),
Expand All @@ -589,7 +614,6 @@ const Notes = (): h.JSX.Element | null => {
});
},
onDelete: () => {
setContextMenuProps(null);
setDeleteNoteModalProps({
noteName,
onCancel: () => setDeleteNoteModalProps(null),
Expand All @@ -601,29 +625,25 @@ const Notes = (): h.JSX.Element | null => {
},
locked: notesProps.notes[noteName].locked ?? false,
onToggleLocked: () => {
setContextMenuProps(null);
if (tabId && notesRef.current) {
setLocked(noteName, !(notesProps.notes[noteName].locked ?? false), tabId, notesRef.current);
}
},
pinned: !!notesProps.notes[noteName].pinnedTime,
onTogglePinnedTime: () => {
setContextMenuProps(null);
if (tabId && notesRef.current) {
setPinnedTime(
noteName,
(notesProps.notes[noteName].pinnedTime ?? undefined) ? undefined : new Date().toISOString(),
notesProps.notes[noteName].pinnedTime ? undefined : new Date().toISOString(),
tabId,
notesRef.current,
);
}
},
onDuplicate: () => {
setContextMenuProps(null);
duplicateNote(noteName);
},
onExport: () => {
setContextMenuProps(null);
exportNote(noteName);
},
})}
Expand Down Expand Up @@ -678,10 +698,6 @@ const Notes = (): h.JSX.Element | null => {
/>
)}

{contextMenuProps && (
<__ContextMenu {...contextMenuProps} />
)}

{renameNoteModalProps && (
<Fragment>
<__RenameNoteModal {...renameNoteModalProps} />
Expand All @@ -703,6 +719,7 @@ const Notes = (): h.JSX.Element | null => {
</Fragment>
)}

<div id="context-menu-container" />
<div id="tooltip-container" />
</Fragment>
);
Expand Down
57 changes: 22 additions & 35 deletions src/notes/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,46 @@
import { h } from "preact";
import { h, ComponentChildren } from "preact";
import { useState, useRef, useEffect } from "preact/hooks";
import clsx from "clsx";
import { t } from "i18n";

export interface ContextMenuProps {
x: number
y: number
onRename: () => void
onDelete: () => void
onToggleLocked: () => void
onTogglePinnedTime: () => void
onDuplicate: () => void
onExport: () => void
locked: boolean
pinned: boolean
children: ComponentChildren
}

const ContextMenu = ({
x, y,
onRename, onDelete, onToggleLocked, onTogglePinnedTime, onDuplicate, onExport,
locked, pinned,
}: ContextMenuProps): h.JSX.Element => {
const [offsetHeight, setOffsetHeight] = useState<number>(0);
interface Offsets {
offsetHeight: number
offsetWidth: number
}

const ContextMenu = ({ x, y, children }: ContextMenuProps): h.JSX.Element => {
const ref = useRef<HTMLDivElement>(null);
const [offsets, setOffsets] = useState<Offsets | undefined>(undefined);

useEffect(() => {
if (!ref.current) {
if (!ref.current || offsets) {
return;
}

if (offsetHeight) {
return; // offsetHeight already set
}

setOffsetHeight(ref.current.offsetHeight);
}, [ref.current, offsetHeight]);
setOffsets({
offsetHeight: ref.current.offsetHeight,
offsetWidth: ref.current.offsetWidth,
});
}, [ref.current]);

return (
<div
id="context-menu"
ref={ref}
style={offsetHeight ? {
left: `${x}px`,
top: (y + offsetHeight < window.innerHeight) ? `${y}px` : "",
bottom: (y + offsetHeight >= window.innerHeight) ? "1em" : "",
style={offsets ? {
left: (x + offsets.offsetWidth < window.innerWidth) ? `${x}px` : undefined,
right: (x + offsets.offsetWidth >= window.innerWidth) ? "1em" : undefined,
top: (y + offsets.offsetHeight < window.innerHeight) ? `${y}px` : undefined,
bottom: (y + offsets.offsetHeight >= window.innerHeight) ? "1em" : undefined,
} : {
opacity: 0, // offsetHeight NOT set, yet
opacity: 0, // offsets NOT known, yet
}}
>
<div className={clsx("action", locked && "disabled")} onClick={() => !locked && onRename()}>{t("Rename")}</div>
<div className={clsx("action", locked && "disabled")} onClick={() => !locked && onDelete()}>{t("Delete")}</div>
<div className="action" onClick={() => onToggleLocked()}>{locked ? t("Unlock") : t("Lock")}</div>
<div className="action" onClick={() => onTogglePinnedTime()}>{pinned ? t("Unpin") : t("Pin")}</div>
<div className="action" onClick={() => onDuplicate()}>{t("Duplicate")}</div>
<div className="action" onClick={() => onExport()}>{t("Export")}</div>
{children}
</div>
);
};
Expand Down
Loading

0 comments on commit 464e44c

Please sign in to comment.