Refactor toolbar extension and styles for improved readability and consistency

- Updated formatting in `toolbar-extension.ts` for better code clarity.
- Enhanced tooltip functionality to ensure it displays correctly based on context.
- Adjusted CSS styles for the mobile FAB and selection toolbar for a more consistent design.
- Ensured proper spacing and alignment in the CSS for better mobile responsiveness.
This commit is contained in:
Im The Justice Man 2025-11-26 16:58:12 +00:00
parent b358ac13d1
commit aab03fc24e
4 changed files with 818 additions and 863 deletions

View file

@ -5,6 +5,5 @@ root = true
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4
indent_style = space
indent_size = 2

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,9 @@
import {
ViewPlugin,
EditorView,
ViewUpdate,
Decoration,
DecorationSet,
ViewPlugin,
EditorView,
ViewUpdate,
Decoration,
DecorationSet,
} from "@codemirror/view";
import { syntaxTree } from "@codemirror/language";
import { App, ButtonComponent } from "obsidian";
@ -13,289 +13,267 @@ import { ToolbarConfig, ContextBinding, ContextType } from "./settings";
* Creates a CodeMirror 6 ViewPlugin that displays a context-aware toolbar at the bottom
* when text is selected or cursor is in a specific context.
*/
export function createToolbarExtension(
app: App,
settings: any
) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
tooltip: HTMLElement | null = null;
app: App;
toolbars: ToolbarConfig[];
contextBindings: ContextBinding[];
useIcons: boolean;
commandIcons: Record<string, string>;
enableHapticFeedback: boolean;
editorContainer: HTMLElement | null = null;
export function createToolbarExtension(app: App, settings: any) {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
tooltip: HTMLElement | null = null;
app: App;
toolbars: ToolbarConfig[];
contextBindings: ContextBinding[];
useIcons: boolean;
commandIcons: Record<string, string>;
enableHapticFeedback: boolean;
editorContainer: HTMLElement | null = null;
constructor(view: EditorView) {
this.decorations = Decoration.none;
this.app = app;
this.toolbars = settings.toolbars;
this.contextBindings = settings.contextBindings;
this.useIcons = settings.useIcons;
this.commandIcons = settings.commandIcons;
this.enableHapticFeedback = settings.enableHapticFeedback;
constructor(view: EditorView) {
this.decorations = Decoration.none;
this.app = app;
this.toolbars = settings.toolbars;
this.contextBindings = settings.contextBindings;
this.useIcons = settings.useIcons;
this.commandIcons = settings.commandIcons;
this.enableHapticFeedback = settings.enableHapticFeedback;
// Find the editor container to anchor the toolbar
this.editorContainer = this.findEditorContainer(view.dom);
// Find the editor container to anchor the toolbar
this.editorContainer = this.findEditorContainer(view.dom);
this.updateTooltip(view);
}
this.updateTooltip(view);
}
hapticFeedback(duration: number = 10) {
if (this.enableHapticFeedback && navigator.vibrate) {
navigator.vibrate(duration);
}
}
hapticFeedback(duration: number = 10) {
if (this.enableHapticFeedback && navigator.vibrate) {
navigator.vibrate(duration);
}
}
findEditorContainer(element: HTMLElement): HTMLElement | null {
// Find the workspace-leaf-content container
let current = element.parentElement;
while (current) {
if (current.classList.contains("workspace-leaf-content")) {
return current;
}
current = current.parentElement;
}
return null;
}
findEditorContainer(element: HTMLElement): HTMLElement | null {
// Find the workspace-leaf-content container
let current = element.parentElement;
while (current) {
if (current.classList.contains("workspace-leaf-content")) {
return current;
}
current = current.parentElement;
}
return null;
}
update(update: ViewUpdate) {
if (
update.selectionSet ||
update.viewportChanged ||
update.docChanged
) {
// Defer tooltip update to avoid reading layout during update
requestAnimationFrame(() => {
this.updateTooltip(update.view);
});
}
}
update(update: ViewUpdate) {
if (
update.selectionSet ||
update.viewportChanged ||
update.docChanged
) {
// Defer tooltip update to avoid reading layout during update
requestAnimationFrame(() => {
this.updateTooltip(update.view);
});
}
}
updateTooltip(view: EditorView) {
const selection = view.state.selection.main;
updateTooltip(view: EditorView) {
const selection = view.state.selection.main;
// Remove existing tooltip if present
if (this.tooltip) {
this.tooltip.remove();
this.tooltip = null;
}
// Remove existing tooltip if present
if (this.tooltip) {
this.tooltip.remove();
this.tooltip = null;
}
// Show toolbar if there's a selection or cursor is in specific context
if (!selection.empty || this.hasContext(view, selection.from)) {
this.showTooltip(view);
}
}
// Show toolbar if there's a selection or cursor is in specific context
if (!selection.empty || this.hasContext(view, selection.from)) {
this.showTooltip(view);
}
}
hasContext(view: EditorView, pos: number): boolean {
const activeContexts = this.getMatchingContexts(view, pos);
// Check if any binding matches the current context
for (const binding of this.contextBindings) {
if (activeContexts.has(binding.contextType)) {
return true;
}
}
return false;
}
hasContext(view: EditorView, pos: number): boolean {
const activeContexts = this.getMatchingContexts(view, pos);
// Check if any binding matches the current context
for (const binding of this.contextBindings) {
if (activeContexts.has(binding.contextType)) {
return true;
}
}
return false;
}
getActiveToolbar(
view: EditorView,
pos: number
): ToolbarConfig | null {
const activeContexts = this.getMatchingContexts(view, pos);
// Collect all matching toolbars and concatenate their commands
const matchingToolbars: ToolbarConfig[] = [];
const seenCommands = new Set<string>();
getActiveToolbar(view: EditorView, pos: number): ToolbarConfig | null {
const activeContexts = this.getMatchingContexts(view, pos);
// Collect all matching toolbars and concatenate their commands
const matchingToolbars: ToolbarConfig[] = [];
const seenCommands = new Set<string>();
for (const binding of this.contextBindings) {
if (activeContexts.has(binding.contextType)) {
const toolbar = this.toolbars.find(
(t) => t.id === binding.toolbarId
);
if (toolbar) {
matchingToolbars.push(toolbar);
}
}
}
for (const binding of this.contextBindings) {
if (activeContexts.has(binding.contextType)) {
const toolbar = this.toolbars.find(
(t) => t.id === binding.toolbarId
);
if (toolbar) {
matchingToolbars.push(toolbar);
}
}
}
// If no matches, return null
if (matchingToolbars.length === 0) {
return null;
}
// If no matches, return null
if (matchingToolbars.length === 0) {
return null;
}
// Concatenate commands from all matching toolbars, removing duplicates
const combinedCommands: string[] = [];
for (const toolbar of matchingToolbars) {
for (const command of toolbar.commands) {
if (!seenCommands.has(command)) {
seenCommands.add(command);
combinedCommands.push(command);
}
}
}
// Concatenate commands from all matching toolbars, removing duplicates
const combinedCommands: string[] = [];
for (const toolbar of matchingToolbars) {
for (const command of toolbar.commands) {
if (!seenCommands.has(command)) {
seenCommands.add(command);
combinedCommands.push(command);
}
}
}
// Return a virtual toolbar with combined commands
return {
id: "combined",
name: "Combined Toolbar",
commands: combinedCommands,
};
}
// Return a virtual toolbar with combined commands
return {
id: "combined",
name: "Combined Toolbar",
commands: combinedCommands,
};
}
getMatchingContexts(
view: EditorView,
pos: number
): Set<ContextType> {
const contexts = new Set<ContextType>();
contexts.add("default");
getMatchingContexts(view: EditorView, pos: number): Set<ContextType> {
const contexts = new Set<ContextType>();
contexts.add("default");
if (!view.state.selection.main.empty) {
contexts.add("selection");
}
if (!view.state.selection.main.empty) {
contexts.add("selection");
}
syntaxTree(view.state).iterate({
from: pos,
to: pos,
enter: (node: any) => {
const nodeName = node.type.name;
syntaxTree(view.state).iterate({
from: pos,
to: pos,
enter: (node: any) => {
const nodeName = node.type.name;
if (
nodeName === "BulletList" ||
nodeName === "OrderedList" ||
nodeName.startsWith(
"HyperMD-list-line_HyperMD-list-line-"
)
) {
contexts.add("list");
}
if (
nodeName === "BulletList" ||
nodeName === "OrderedList" ||
nodeName.startsWith("HyperMD-list-line_HyperMD-list-line-")
) {
contexts.add("list");
}
if (
nodeName === "Task" ||
nodeName.includes("HyperMD-task-line")
) {
contexts.add("task");
}
if (nodeName === "Task" || nodeName.includes("HyperMD-task-line")) {
contexts.add("task");
}
if (
nodeName.startsWith("ATXHeading") ||
nodeName === "SetextHeading" ||
nodeName.startsWith("HyperMD-header")
) {
contexts.add("heading");
}
if (
nodeName.startsWith("ATXHeading") ||
nodeName === "SetextHeading" ||
nodeName.startsWith("HyperMD-header")
) {
contexts.add("heading");
}
if (
nodeName === "FencedCode" ||
nodeName === "CodeBlock" ||
nodeName.includes("HyperMD-codeblock")
) {
contexts.add("code-block");
}
if (
nodeName === "FencedCode" ||
nodeName === "CodeBlock" ||
nodeName.includes("HyperMD-codeblock")
) {
contexts.add("code-block");
}
if (
nodeName === "Table" ||
nodeName.startsWith("Table") ||
nodeName.includes("HyperMD-table")
) {
contexts.add("table");
}
if (
nodeName === "Table" ||
nodeName.startsWith("Table") ||
nodeName.includes("HyperMD-table")
) {
contexts.add("table");
}
if (
nodeName === "Blockquote" ||
nodeName === "QuoteMark" ||
nodeName.includes("HyperMD-quote")
) {
contexts.add("blockquote");
}
if (
nodeName === "Blockquote" ||
nodeName === "QuoteMark" ||
nodeName.includes("HyperMD-quote")
) {
contexts.add("blockquote");
}
if (
nodeName === "Link" ||
nodeName.includes("link") ||
nodeName.includes("URL") ||
nodeName.includes("HyperMD-link")
) {
contexts.add("link");
}
},
});
if (
nodeName === "Link" ||
nodeName.includes("link") ||
nodeName.includes("URL") ||
nodeName.includes("HyperMD-link")
) {
contexts.add("link");
}
},
});
return contexts;
}
return contexts;
}
showTooltip(view: EditorView) {
const selection = view.state.selection.main;
showTooltip(view: EditorView) {
const selection = view.state.selection.main;
// Get the active toolbar based on context
const activeToolbar = this.getActiveToolbar(
view,
selection.from
);
// Get the active toolbar based on context
const activeToolbar = this.getActiveToolbar(view, selection.from);
if (!activeToolbar || activeToolbar.commands.length === 0) {
return;
}
if (!activeToolbar || activeToolbar.commands.length === 0) {
return;
}
// Create tooltip element
this.tooltip = (this.editorContainer || view.dom).createDiv({
cls: "mobile-selection-toolbar",
attr: { "data-toolbar-id": activeToolbar.id },
});
// Create tooltip element
this.tooltip = (this.editorContainer || view.dom).createDiv({
cls: "mobile-selection-toolbar",
attr: { "data-toolbar-id": activeToolbar.id },
});
// Get all available commands
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const commands = (this.app as any).commands?.commands || {};
// Get all available commands
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const commands = (this.app as any).commands?.commands || {};
// Add command buttons
activeToolbar.commands.forEach((commandId) => {
const command = commands[commandId];
const iconToUse =
this.commandIcons[commandId] || command.icon;
if (command && this.tooltip) {
if (this.useIcons && iconToUse) {
new ButtonComponent(this.tooltip)
/* .setClass("mobile-toolbar-button") */
.setIcon(iconToUse)
.setTooltip(command.name || commandId)
.onClick((e) => {
// Haptic feedback on button click
this.hapticFeedback(10);
// Execute the command
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(
this.app as any
).commands?.executeCommandById(commandId);
});
} else {
new ButtonComponent(this.tooltip)
/* .setClass("mobile-toolbar-button") */
.setButtonText(command.name || commandId)
.setTooltip(command.name || commandId)
.onClick((e) => {
// Haptic feedback on button click
this.hapticFeedback(10);
// Execute the command
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(
this.app as any
).commands?.executeCommandById(commandId);
});
}
}
});
}
// Add command buttons
activeToolbar.commands.forEach((commandId) => {
const command = commands[commandId];
const iconToUse = this.commandIcons[commandId] || command.icon;
if (command && this.tooltip) {
if (this.useIcons && iconToUse) {
new ButtonComponent(this.tooltip)
/* .setClass("mobile-toolbar-button") */
.setIcon(iconToUse)
.setTooltip(command.name || commandId)
.onClick((e) => {
// Haptic feedback on button click
this.hapticFeedback(10);
// Execute the command
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.app as any).commands?.executeCommandById(commandId);
});
} else {
new ButtonComponent(this.tooltip)
/* .setClass("mobile-toolbar-button") */
.setButtonText(command.name || commandId)
.setTooltip(command.name || commandId)
.onClick((e) => {
// Haptic feedback on button click
this.hapticFeedback(10);
// Execute the command
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.app as any).commands?.executeCommandById(commandId);
});
}
}
});
}
destroy() {
if (this.tooltip) {
this.tooltip.remove();
this.tooltip = null;
}
}
},
{
// No decorations needed for this plugin
}
);
destroy() {
if (this.tooltip) {
this.tooltip.remove();
this.tooltip = null;
}
}
},
{
// No decorations needed for this plugin
}
);
}

View file

@ -5,134 +5,134 @@ Includes FAB and selection toolbar styles with mobile-friendly design
/* Ensure workspace-leaf-content is positioned for absolute children */
.workspace-leaf-content {
position: relative;
position: relative;
}
/* Floating Action Button (FAB) */
.mobile-fab {
position: absolute;
bottom: calc(20px + env(safe-area-inset-bottom));
right: 20px;
width: 56px;
height: 56px;
border-radius: 50%;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: absolute;
bottom: calc(20px + env(safe-area-inset-bottom));
right: 20px;
width: 56px;
height: 56px;
border-radius: 50%;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.mobile-fab:hover {
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
transform: scale(1.05);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
}
.mobile-fab:active {
transform: scale(0.95);
transform: scale(0.95);
}
.mobile-fab svg {
width: 24px;
height: 24px;
width: 24px;
height: 24px;
}
/* Selection Toolbar */
.mobile-selection-toolbar {
display: flex;
gap: 8px;
padding: 8px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
position: absolute;
bottom: calc(10px + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex;
gap: 8px;
padding: 8px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
position: absolute;
bottom: calc(10px + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
z-index: 1000;
}
.mobile-toolbar-button {
padding: 6px 12px;
background-color: var(--interactive-normal);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
min-height: 36px;
padding: 6px 12px;
background-color: var(--interactive-normal);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
min-width: 36px;
min-height: 36px;
}
.mobile-toolbar-button:hover {
background-color: var(--interactive-hover);
background-color: var(--interactive-hover);
}
.mobile-toolbar-button:active {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}
.mobile-toolbar-icon {
display: flex;
align-items: center;
justify-content: center;
display: flex;
align-items: center;
justify-content: center;
}
.mobile-toolbar-icon svg {
width: 18px;
height: 18px;
width: 18px;
height: 18px;
}
/* Settings Page Styles */
.mobile-toolbar-section {
margin: 1.5em 0;
padding: 1em;
background-color: var(--background-secondary);
border-radius: 8px;
border: 1px solid var(--background-modifier-border);
margin: 1.5em 0;
padding: 1em;
background-color: var(--background-secondary);
border-radius: 8px;
border: 1px solid var(--background-modifier-border);
}
.mobile-toolbar-header {
font-weight: 600;
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 0.5em;
margin-bottom: 0.5em;
font-weight: 600;
border-bottom: 1px solid var(--background-modifier-border);
padding-bottom: 0.5em;
margin-bottom: 0.5em;
}
.mobile-command-list {
margin: 0.5em 0;
margin: 0.5em 0;
}
.mobile-add-command-btn {
margin-top: 0.5em;
margin-top: 0.5em;
}
/* Settings Drag and Drop */
.mobile-plugin-draggable-item {
cursor: grab;
cursor: grab;
}
.mobile-plugin-draggable-item.is-dragging {
opacity: 0.5;
cursor: grabbing;
opacity: 0.5;
cursor: grabbing;
}
.mobile-plugin-draggable-item.drag-over-top {
border-top: 2px solid var(--interactive-accent);
border-top: 2px solid var(--interactive-accent);
}
.mobile-plugin-draggable-item.drag-over-bottom {
border-bottom: 2px solid var(--interactive-accent);
border-bottom: 2px solid var(--interactive-accent);
}