More tweaks

pull/119/head
Zef Hemel 2022-11-16 17:17:07 +01:00
parent 6cad99f097
commit 7fefc212a8
14 changed files with 182 additions and 187 deletions

View File

@ -198,6 +198,13 @@ functions:
description: Turn line into h3 header description: Turn line into h3 header
match: "^#*\\s*" match: "^#*\\s*"
replace: "### " replace: "### "
makeH4:
redirect: applyLineReplace
slashCommand:
name: h4
description: Turn line into h4 header
match: "^#*\\s*"
replace: "#### "
newPage: newPage:
path: ./page.ts:newPageCommand path: ./page.ts:newPageCommand

View File

@ -91,7 +91,6 @@ export function taskToggle(event: ClickEvent) {
export function previewTaskToggle(eventString: string) { export function previewTaskToggle(eventString: string) {
const [eventName, pos] = JSON.parse(eventString); const [eventName, pos] = JSON.parse(eventString);
if (eventName === "task") { if (eventName === "task") {
console.log("Gotta toggle a task at", pos);
return taskToggleAtPos(+pos); return taskToggleAtPos(+pos);
} }
} }
@ -107,9 +106,6 @@ async function toggleTaskMarker(node: ParseTree, moveToPos: number) {
to: node.to, to: node.to,
insert: changeTo, insert: changeTo,
}, },
selection: {
anchor: moveToPos,
},
}); });
const parentWikiLinks = collectNodesMatching( const parentWikiLinks = collectNodesMatching(

View File

@ -1,4 +1,6 @@
import type { ClickEvent } from "../../plug-api/app_event.ts";
import type { Extension } from "../deps.ts"; import type { Extension } from "../deps.ts";
import { Editor } from "../editor.tsx";
import { blockquotePlugin } from "./block_quote.ts"; import { blockquotePlugin } from "./block_quote.ts";
import { directivePlugin } from "./directive.ts"; import { directivePlugin } from "./directive.ts";
import { hideHeaderMarkPlugin, hideMarks } from "./hide_mark.ts"; import { hideHeaderMarkPlugin, hideMarks } from "./hide_mark.ts";
@ -9,15 +11,30 @@ import { tablePlugin } from "./table.ts";
import { taskListPlugin } from "./task.ts"; import { taskListPlugin } from "./task.ts";
import { cleanWikiLinkPlugin } from "./wiki_link.ts"; import { cleanWikiLinkPlugin } from "./wiki_link.ts";
export const cleanModePlugs = [ export function cleanModePlugins(editor: Editor) {
goToLinkPlugin, return [
directivePlugin, goToLinkPlugin,
blockquotePlugin, directivePlugin,
hideMarks(), blockquotePlugin,
hideHeaderMarkPlugin, hideMarks(),
hideImageNodePlugin, hideHeaderMarkPlugin,
taskListPlugin, hideImageNodePlugin,
listBulletPlugin, taskListPlugin({
tablePlugin, // TODO: Move this logic elsewhere?
cleanWikiLinkPlugin(), onCheckboxClick: (pos) => {
] as Extension[]; const clickEvent: ClickEvent = {
page: editor.currentPage!,
altKey: false,
ctrlKey: false,
metaKey: false,
pos: pos,
};
// Propagate click event from checkbox
editor.dispatchAppEvent("page:click", clickEvent);
},
}),
listBulletPlugin,
tablePlugin,
cleanWikiLinkPlugin(),
] as Extension[];
}

View File

@ -6,41 +6,38 @@ import {
Decoration, Decoration,
DecorationSet, DecorationSet,
EditorView, EditorView,
syntaxTree,
ViewPlugin, ViewPlugin,
ViewUpdate, ViewUpdate,
} from "../deps.ts"; } from "../deps.ts";
import { checkRangeOverlap, invisibleDecoration } from "./util.ts"; import {
checkRangeOverlap,
invisibleDecoration,
iterateTreeInVisibleRanges,
} from "./util.ts";
function getLinkAnchor(view: EditorView) { function getLinkAnchor(view: EditorView) {
const widgets: any[] = []; const widgets: any[] = [];
for (const { from, to } of view.visibleRanges) { iterateTreeInVisibleRanges(view, {
syntaxTree(view.state).iterate({ enter: ({ type, from, to, node }) => {
from, if (type.name !== "URL") return;
to, const parent = node.parent;
enter: ({ type, from, to, node }) => { const blackListedParents = ["Image"];
if (type.name !== "URL") return; if (parent && !blackListedParents.includes(parent.name)) {
const parent = node.parent; const marks = parent.getChildren("LinkMark");
const blackListedParents = ["Image"]; const ranges = view.state.selection.ranges;
if (parent && !blackListedParents.includes(parent.name)) { const cursorOverlaps = ranges.some(({ from, to }) =>
const marks = parent.getChildren("LinkMark"); checkRangeOverlap([from, to], [parent.from, parent.to])
const ranges = view.state.selection.ranges; );
const cursorOverlaps = ranges.some(({ from, to }) => if (!cursorOverlaps) {
checkRangeOverlap([from, to], [parent.from, parent.to]) widgets.push(
...marks.map(({ from, to }) => invisibleDecoration.range(from, to)),
invisibleDecoration.range(from, to),
); );
if (!cursorOverlaps) {
widgets.push(
...marks.map(({ from, to }) =>
invisibleDecoration.range(from, to)
),
invisibleDecoration.range(from, to),
);
}
} }
}, }
}); },
} });
return Decoration.set(widgets, true); return Decoration.set(widgets, true);
} }

View File

@ -65,6 +65,9 @@ class TablePlugin {
const firstLine = lines[0], lastLine = lines[lines.length - 1]; const firstLine = lines[0], lastLine = lines[lines.length - 1];
// In case of doubt, back out
if (!firstLine || !lastLine) return;
widgets.push(invisibleDecoration.range(firstLine.from, firstLine.to)); widgets.push(invisibleDecoration.range(firstLine.from, firstLine.to));
widgets.push(invisibleDecoration.range(lastLine.from, lastLine.to)); widgets.push(invisibleDecoration.range(lastLine.from, lastLine.to));

View File

@ -1,5 +1,4 @@
import { import {
ChangeSpec,
Decoration, Decoration,
DecorationSet, DecorationSet,
EditorView, EditorView,
@ -11,87 +10,97 @@ import {
} from "../deps.ts"; } from "../deps.ts";
import { isCursorInRange, iterateTreeInVisibleRanges } from "./util.ts"; import { isCursorInRange, iterateTreeInVisibleRanges } from "./util.ts";
/** // TODO: Find a nicer way to inject this on task handler into the class
* Plugin to add checkboxes in task lists. function TaskListsPluginFactory(onCheckboxClick: (pos: number) => void) {
*/ return class TaskListsPlugin {
class TaskListsPlugin { decorations: DecorationSet = Decoration.none;
decorations: DecorationSet = Decoration.none; constructor(
constructor(view: EditorView) { view: EditorView,
this.decorations = this.addCheckboxes(view); ) {
} this.decorations = this.addCheckboxes(view);
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = this.addCheckboxes(update.view);
} }
} update(update: ViewUpdate) {
addCheckboxes(view: EditorView) { if (update.docChanged || update.viewportChanged || update.selectionSet) {
const widgets: any[] = []; this.decorations = this.addCheckboxes(update.view);
iterateTreeInVisibleRanges(view, { }
enter: this.iterateTree(view, widgets), }
}); addCheckboxes(view: EditorView) {
return Decoration.set(widgets, true); const widgets: any[] = [];
} iterateTreeInVisibleRanges(view, {
enter: this.iterateTree(view, widgets),
private iterateTree(view: EditorView, widgets: any[]) {
return ({ type, from, to, node }: SyntaxNodeRef) => {
if (type.name !== "Task") return;
let checked = false;
// Iterate inside the task node to find the checkbox
node.toTree().iterate({
enter: (ref) => iterateInner(ref.type, ref.from, ref.to),
}); });
if (checked) { return Decoration.set(widgets, true);
widgets.push( }
Decoration.mark({
tagName: "span",
class: "cm-task-checked",
}).range(from, to),
);
}
function iterateInner(type: NodeType, nfrom: number, nto: number) { private iterateTree(view: EditorView, widgets: any[]) {
if (type.name !== "TaskMarker") return; return ({ type, from, to, node }: SyntaxNodeRef) => {
if (isCursorInRange(view.state, [from + nfrom, from + nto])) return; if (type.name !== "Task") return;
const checkbox = view.state.sliceDoc(from + nfrom, from + nto); let checked = false;
// Checkbox is checked if it has a 'x' in between the [] // Iterate inside the task node to find the checkbox
if ("xX".includes(checkbox[1])) checked = true; node.toTree().iterate({
const dec = Decoration.replace({ enter: (ref) => iterateInner(ref.type, ref.from, ref.to),
widget: new CheckboxWidget(checked, from + nfrom + 1),
}); });
widgets.push(dec.range(from + nfrom, from + nto)); if (checked) {
} widgets.push(
}; Decoration.mark({
} tagName: "span",
class: "cm-task-checked",
}).range(from, to),
);
}
function iterateInner(type: NodeType, nfrom: number, nto: number) {
if (type.name !== "TaskMarker") return;
if (isCursorInRange(view.state, [from + nfrom, from + nto])) return;
const checkbox = view.state.sliceDoc(from + nfrom, from + nto);
// Checkbox is checked if it has a 'x' in between the []
if ("xX".includes(checkbox[1])) checked = true;
const dec = Decoration.replace({
widget: new CheckboxWidget(
checked,
from + nfrom + 1,
onCheckboxClick,
),
});
widgets.push(dec.range(from + nfrom, from + nto));
}
};
}
};
} }
/** /**
* Widget to render checkbox for a task list item. * Widget to render checkbox for a task list item.
*/ */
class CheckboxWidget extends WidgetType { class CheckboxWidget extends WidgetType {
constructor(public checked: boolean, readonly pos: number) { constructor(
public checked: boolean,
readonly pos: number,
readonly clickCallback: (pos: number) => void,
) {
super(); super();
} }
toDOM(view: EditorView): HTMLElement { toDOM(_view: EditorView): HTMLElement {
const wrap = document.createElement("span"); const wrap = document.createElement("span");
wrap.classList.add("sb-checkbox"); wrap.classList.add("sb-checkbox");
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
checkbox.checked = this.checked; checkbox.checked = this.checked;
checkbox.addEventListener("click", ({ target }) => { checkbox.addEventListener("click", (e) => {
const change: ChangeSpec = { // Let the click handler handle this
from: this.pos, e.stopPropagation();
to: this.pos + 1,
insert: this.checked ? " " : "x", this.clickCallback(this.pos);
};
view.dispatch({ changes: change });
this.checked = !this.checked;
(target as HTMLInputElement).checked = this.checked;
}); });
wrap.appendChild(checkbox); wrap.appendChild(checkbox);
return wrap; return wrap;
} }
} }
export const taskListPlugin = ViewPlugin.fromClass(TaskListsPlugin, { export function taskListPlugin(
decorations: (v) => v.decorations, { onCheckboxClick }: { onCheckboxClick: (pos: number) => void },
}); ) {
return ViewPlugin.fromClass(TaskListsPluginFactory(onCheckboxClick), {
decorations: (v) => v.decorations,
});
}

View File

@ -61,9 +61,10 @@ export function iterateTreeInVisibleRanges(
leave?(node: SyntaxNodeRef): void; leave?(node: SyntaxNodeRef): void;
}, },
) { ) {
for (const { from, to } of view.visibleRanges) { // for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({ ...iterateFns, from, to }); // syntaxTree(view.state).iterate({ ...iterateFns, from, to });
} // }
syntaxTree(view.state).iterate(iterateFns);
} }
/** /**

View File

@ -6,7 +6,6 @@ import {
ViewUpdate, ViewUpdate,
} from "../deps.ts"; } from "../deps.ts";
import { import {
checkRangeOverlap,
invisibleDecoration, invisibleDecoration,
isCursorInRange, isCursorInRange,
iterateTreeInVisibleRanges, iterateTreeInVisibleRanges,

View File

@ -135,7 +135,7 @@ export function FilterList({
searchBoxRef.current!.focus(); searchBoxRef.current!.focus();
} }
}} }}
onKeyDown={(e) => { onKeyUp={(e) => {
// console.log("Key up", / e); // console.log("Key up", / e);
if (onKeyPress) { if (onKeyPress) {
onKeyPress(e.key, text); onKeyPress(e.key, text);
@ -178,12 +178,13 @@ export function FilterList({
} }
break; break;
default: default:
setTimeout(() => { updateFilter((e.target as any).value);
updateFilter((e.target as any).value);
});
} }
e.stopPropagation(); e.stopPropagation();
}} }}
onKeyDown={(e) => {
e.stopPropagation();
}}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
</div> </div>

View File

@ -45,38 +45,12 @@ export function TopBar({
}) { }) {
const [theme, setTheme] = useState<string>(localStorage.theme ?? "light"); const [theme, setTheme] = useState<string>(localStorage.theme ?? "light");
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [editMode, setEditMode] = useState<boolean>(false);
const isMac = isMacLike(); const isMac = isMacLike();
useEffect(() => {
if (editMode) {
setTimeout(() => {
if (inputRef.current) {
console.log("Going to focus");
inputRef.current!.focus();
}
}, 0);
}
}, [editMode]);
return ( return (
<div id="sb-top"> <div id="sb-top">
{lhs} {lhs}
<div <div className="main">
className="main"
onClick={(e) => {
if (!editMode) {
setEditMode(true);
setTimeout(() => {
if (inputRef.current) {
console.log("Going to dispatch click event again");
inputRef.current!.dispatchEvent(e);
}
}, 100);
}
}}
>
<div className="inner"> <div className="inner">
<span <span
className={`sb-current-page ${ className={`sb-current-page ${
@ -87,29 +61,21 @@ export function TopBar({
: "sb-saved" : "sb-saved"
}`} }`}
> >
{editMode <input
? ( type="text"
<input ref={inputRef}
type="text" value={pageName}
ref={inputRef} className="sb-edit-page-name"
value={pageName} onKeyDown={(e) => {
className="sb-edit-page-name" console.log("Key press", e);
onBlur={() => { e.stopPropagation();
setEditMode(false); if (e.key === "Enter") {
}} e.preventDefault();
onKeyDown={(e) => { const newName = (e.target as any).value;
console.log("Key press", e); onRename(newName);
e.stopPropagation(); }
if (e.key === "Enter") { }}
e.preventDefault(); />
const newName = (e.target as any).value;
onRename(newName);
setEditMode(false);
}
}}
/>
)
: pageName}
</span> </span>
{notifications.length > 0 && ( {notifications.length > 0 && (
<div className="sb-notifications"> <div className="sb-notifications">

View File

@ -99,7 +99,7 @@ import {
import { inlineImagesPlugin } from "./cm_plugins/inline_image.ts"; import { inlineImagesPlugin } from "./cm_plugins/inline_image.ts";
import { lineWrapper } from "./cm_plugins/line_wrapper.ts"; import { lineWrapper } from "./cm_plugins/line_wrapper.ts";
import { smartQuoteKeymap } from "./cm_plugins/smart_quotes.ts"; import { smartQuoteKeymap } from "./cm_plugins/smart_quotes.ts";
import { cleanModePlugs } from "./cm_plugins/clean.ts"; import { cleanModePlugins } from "./cm_plugins/clean.ts";
import customMarkdownStyle from "./style.ts"; import customMarkdownStyle from "./style.ts";
// Real-time collaboration // Real-time collaboration
@ -463,12 +463,13 @@ export class Editor {
drawSelection(), drawSelection(),
dropCursor(), dropCursor(),
indentOnInput(), indentOnInput(),
...cleanModePlugs, ...cleanModePlugins(this),
EditorView.lineWrapping, EditorView.lineWrapping,
lineWrapper([ lineWrapper([
{ selector: "ATXHeading1", class: "sb-line-h1" }, { selector: "ATXHeading1", class: "sb-line-h1" },
{ selector: "ATXHeading2", class: "sb-line-h2" }, { selector: "ATXHeading2", class: "sb-line-h2" },
{ selector: "ATXHeading3", class: "sb-line-h3" }, { selector: "ATXHeading3", class: "sb-line-h3" },
{ selector: "ATXHeading4", class: "sb-line-h4" },
{ selector: "ListItem", class: "sb-line-li", nesting: true }, { selector: "ListItem", class: "sb-line-li", nesting: true },
{ selector: "Blockquote", class: "sb-line-blockquote" }, { selector: "Blockquote", class: "sb-line-blockquote" },
{ selector: "Task", class: "sb-line-task" }, { selector: "Task", class: "sb-line-task" },
@ -510,23 +511,6 @@ export class Editor {
return true; return true;
}, },
}, },
{
key: "Ctrl-l",
mac: "Cmd-l",
run: (): boolean => {
this.editorView?.dispatch({
effects: [
EditorView.scrollIntoView(
this.editorView.state.selection.main.anchor,
{
y: "center",
},
),
],
});
return true;
},
},
]), ]),
EditorView.domEventHandlers({ EditorView.domEventHandlers({

View File

@ -20,7 +20,7 @@ export type SlashCommandHookT = {
slashCommand?: SlashCommandDef; slashCommand?: SlashCommandDef;
}; };
const slashCommandRegexp = /\/[\w\-]*/; const slashCommandRegexp = /([^\w]|^)\/[\w\-]*/;
export class SlashCommandHook implements Hook<SlashCommandHookT> { export class SlashCommandHook implements Hook<SlashCommandHookT> {
slashCommands = new Map<string, AppSlashCommand>(); slashCommands = new Map<string, AppSlashCommand>();

View File

@ -208,3 +208,8 @@
left: -20px; left: -20px;
} }
} }
.cm-scroller {
// Give some breathing space at the bottom of the screen
padding-bottom: 20em;
}

View File

@ -129,20 +129,25 @@
} }
.sb-header-inside.sb-line-h1 { .sb-header-inside.sb-line-h1 {
margin-left: -2ch; text-indent: -2ch;
} }
.sb-header-inside.sb-line-h2 { .sb-header-inside.sb-line-h2 {
margin-left: -3ch; text-indent: -3ch;
} }
.sb-header-inside.sb-line-h3 { .sb-header-inside.sb-line-h3 {
margin-left: -4ch; text-indent: -4ch;
}
.sb-header-inside.sb-line-h4 {
text-indent: -5ch;
} }
.sb-line-h1, .sb-line-h1,
.sb-line-h2, .sb-line-h2,
.sb-line-h3 { .sb-line-h3,
.sb-line-h4 {
// background-color: rgba(0, 30, 77, 0.5); // background-color: rgba(0, 30, 77, 0.5);
color: #333; color: #333;
font-weight: bold; font-weight: bold;
@ -151,7 +156,8 @@
.sb-line-h1 .sb-meta, .sb-line-h1 .sb-meta,
.sb-line-h2 .sb-meta, .sb-line-h2 .sb-meta,
.sb-line-h3 .sb-meta { .sb-line-h3 .sb-meta,
.sb-line-h4 .sb-meta {
color: #a1a1a0; color: #a1a1a0;
} }
@ -167,6 +173,10 @@
font-size: 1.1em; font-size: 1.1em;
} }
.sb-line-h4 {
font-size: 1em;
}
.sb-hashtag { .sb-hashtag {
color: blue; color: blue;
} }