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
match: "^#*\\s*"
replace: "### "
makeH4:
redirect: applyLineReplace
slashCommand:
name: h4
description: Turn line into h4 header
match: "^#*\\s*"
replace: "#### "
newPage:
path: ./page.ts:newPageCommand

View File

@ -91,7 +91,6 @@ export function taskToggle(event: ClickEvent) {
export function previewTaskToggle(eventString: string) {
const [eventName, pos] = JSON.parse(eventString);
if (eventName === "task") {
console.log("Gotta toggle a task at", pos);
return taskToggleAtPos(+pos);
}
}
@ -107,9 +106,6 @@ async function toggleTaskMarker(node: ParseTree, moveToPos: number) {
to: node.to,
insert: changeTo,
},
selection: {
anchor: moveToPos,
},
});
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 { Editor } from "../editor.tsx";
import { blockquotePlugin } from "./block_quote.ts";
import { directivePlugin } from "./directive.ts";
import { hideHeaderMarkPlugin, hideMarks } from "./hide_mark.ts";
@ -9,15 +11,30 @@ import { tablePlugin } from "./table.ts";
import { taskListPlugin } from "./task.ts";
import { cleanWikiLinkPlugin } from "./wiki_link.ts";
export const cleanModePlugs = [
goToLinkPlugin,
directivePlugin,
blockquotePlugin,
hideMarks(),
hideHeaderMarkPlugin,
hideImageNodePlugin,
taskListPlugin,
listBulletPlugin,
tablePlugin,
cleanWikiLinkPlugin(),
] as Extension[];
export function cleanModePlugins(editor: Editor) {
return [
goToLinkPlugin,
directivePlugin,
blockquotePlugin,
hideMarks(),
hideHeaderMarkPlugin,
hideImageNodePlugin,
taskListPlugin({
// TODO: Move this logic elsewhere?
onCheckboxClick: (pos) => {
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,
DecorationSet,
EditorView,
syntaxTree,
ViewPlugin,
ViewUpdate,
} from "../deps.ts";
import { checkRangeOverlap, invisibleDecoration } from "./util.ts";
import {
checkRangeOverlap,
invisibleDecoration,
iterateTreeInVisibleRanges,
} from "./util.ts";
function getLinkAnchor(view: EditorView) {
const widgets: any[] = [];
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: ({ type, from, to, node }) => {
if (type.name !== "URL") return;
const parent = node.parent;
const blackListedParents = ["Image"];
if (parent && !blackListedParents.includes(parent.name)) {
const marks = parent.getChildren("LinkMark");
const ranges = view.state.selection.ranges;
const cursorOverlaps = ranges.some(({ from, to }) =>
checkRangeOverlap([from, to], [parent.from, parent.to])
iterateTreeInVisibleRanges(view, {
enter: ({ type, from, to, node }) => {
if (type.name !== "URL") return;
const parent = node.parent;
const blackListedParents = ["Image"];
if (parent && !blackListedParents.includes(parent.name)) {
const marks = parent.getChildren("LinkMark");
const ranges = view.state.selection.ranges;
const cursorOverlaps = ranges.some(({ from, to }) =>
checkRangeOverlap([from, to], [parent.from, parent.to])
);
if (!cursorOverlaps) {
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);
}

View File

@ -65,6 +65,9 @@ class TablePlugin {
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(lastLine.from, lastLine.to));

View File

@ -1,5 +1,4 @@
import {
ChangeSpec,
Decoration,
DecorationSet,
EditorView,
@ -11,87 +10,97 @@ import {
} from "../deps.ts";
import { isCursorInRange, iterateTreeInVisibleRanges } from "./util.ts";
/**
* Plugin to add checkboxes in task lists.
*/
class TaskListsPlugin {
decorations: DecorationSet = Decoration.none;
constructor(view: EditorView) {
this.decorations = this.addCheckboxes(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = this.addCheckboxes(update.view);
// TODO: Find a nicer way to inject this on task handler into the class
function TaskListsPluginFactory(onCheckboxClick: (pos: number) => void) {
return class TaskListsPlugin {
decorations: DecorationSet = Decoration.none;
constructor(
view: EditorView,
) {
this.decorations = this.addCheckboxes(view);
}
}
addCheckboxes(view: EditorView) {
const widgets: any[] = [];
iterateTreeInVisibleRanges(view, {
enter: this.iterateTree(view, widgets),
});
return Decoration.set(widgets, true);
}
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),
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged || update.selectionSet) {
this.decorations = this.addCheckboxes(update.view);
}
}
addCheckboxes(view: EditorView) {
const widgets: any[] = [];
iterateTreeInVisibleRanges(view, {
enter: this.iterateTree(view, widgets),
});
if (checked) {
widgets.push(
Decoration.mark({
tagName: "span",
class: "cm-task-checked",
}).range(from, to),
);
}
return Decoration.set(widgets, true);
}
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),
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),
});
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.
*/
class CheckboxWidget extends WidgetType {
constructor(public checked: boolean, readonly pos: number) {
constructor(
public checked: boolean,
readonly pos: number,
readonly clickCallback: (pos: number) => void,
) {
super();
}
toDOM(view: EditorView): HTMLElement {
toDOM(_view: EditorView): HTMLElement {
const wrap = document.createElement("span");
wrap.classList.add("sb-checkbox");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = this.checked;
checkbox.addEventListener("click", ({ target }) => {
const change: ChangeSpec = {
from: this.pos,
to: this.pos + 1,
insert: this.checked ? " " : "x",
};
view.dispatch({ changes: change });
this.checked = !this.checked;
(target as HTMLInputElement).checked = this.checked;
checkbox.addEventListener("click", (e) => {
// Let the click handler handle this
e.stopPropagation();
this.clickCallback(this.pos);
});
wrap.appendChild(checkbox);
return wrap;
}
}
export const taskListPlugin = ViewPlugin.fromClass(TaskListsPlugin, {
decorations: (v) => v.decorations,
});
export function taskListPlugin(
{ 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;
},
) {
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({ ...iterateFns, from, to });
}
// for (const { from, to } of view.visibleRanges) {
// syntaxTree(view.state).iterate({ ...iterateFns, from, to });
// }
syntaxTree(view.state).iterate(iterateFns);
}
/**

View File

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

View File

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

View File

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

View File

@ -99,7 +99,7 @@ import {
import { inlineImagesPlugin } from "./cm_plugins/inline_image.ts";
import { lineWrapper } from "./cm_plugins/line_wrapper.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";
// Real-time collaboration
@ -463,12 +463,13 @@ export class Editor {
drawSelection(),
dropCursor(),
indentOnInput(),
...cleanModePlugs,
...cleanModePlugins(this),
EditorView.lineWrapping,
lineWrapper([
{ selector: "ATXHeading1", class: "sb-line-h1" },
{ selector: "ATXHeading2", class: "sb-line-h2" },
{ selector: "ATXHeading3", class: "sb-line-h3" },
{ selector: "ATXHeading4", class: "sb-line-h4" },
{ selector: "ListItem", class: "sb-line-li", nesting: true },
{ selector: "Blockquote", class: "sb-line-blockquote" },
{ selector: "Task", class: "sb-line-task" },
@ -510,23 +511,6 @@ export class Editor {
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({

View File

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

View File

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