More tweaks
parent
6cad99f097
commit
7fefc212a8
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
ViewUpdate,
|
||||
} from "../deps.ts";
|
||||
import {
|
||||
checkRangeOverlap,
|
||||
invisibleDecoration,
|
||||
isCursorInRange,
|
||||
iterateTreeInVisibleRanges,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -208,3 +208,8 @@
|
|||
left: -20px;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
// Give some breathing space at the bottom of the screen
|
||||
padding-bottom: 20em;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue