diff --git a/README.md b/README.md
index 6e688e348..ef92a3044 100644
--- a/README.md
+++ b/README.md
@@ -10,11 +10,11 @@
## Overview
-Notesnook is a free (as in speech) & open source note taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
+Notesnook is a free (as in speech) & open-source note-taking app focused on user privacy & ease of use. To ensure zero knowledge principles, Notesnook encrypts everything on your device using `XChaCha20-Poly1305` & `Argon2`.
-Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. Our goal is to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
+Notesnook is our **proof** that privacy does _not_ (always) have to come at the cost of convenience. We aim to provide users peace of mind & 100% confidence that their notes are safe and secure. The decision to go fully open source is one of the most crucial steps towards that.
-This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full features list or screenshots, please check the [website](https://notesnook.com/).
+This repository contains all the code required to build & use the Notesnook web, desktop & mobile clients. If you are looking for a full feature list or screenshots, please check the [website](https://notesnook.com/).
## Developer guide
diff --git a/apps/mobile/README.md b/apps/mobile/README.md
index 8d2556674..a138a3034 100644
--- a/apps/mobile/README.md
+++ b/apps/mobile/README.md
@@ -19,9 +19,9 @@
## Build instructions
-> **Before you start it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
+> **Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
-### Setting up development environment
+### Setting up the development environment
Requirements:
@@ -57,7 +57,7 @@ npm install
### Running the app on Android
-[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already and then run the following command to start the app in the Emulator:
+[Setup an Android emulator from Android Studio](https://developer.android.com/studio/run/managing-avds) if you haven't already, and then run the following command to start the app in the Emulator:
```bash
npm run start:android
@@ -78,11 +78,11 @@ npm run start:ios
## Developer guide
-> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
+> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
### The tech stack
-We try to keep the stack as lean as possible
+We try to keep the stack as lean as possible:
1. React Native
2. Typescript/Javascript
@@ -95,17 +95,17 @@ We try to keep the stack as lean as possible
The app codebase is distributed over two primary directories. `native/` and `app/`.
-- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development and packaging. Any react-native dependency that has native code i.e android & ios folders, is installed here.
+- `native/`: Includes `android/` and `ios/` folders and everything related to react native core functionality like bundling, development, and packaging. Any react-native dependency with native code, i.e., android & ios folders, is installed here.
-- `app/`: Includes all the app code other than the native part. All JS only dependencies are installed here.
- - `components/`: Each component serves a specific purpose in the app UI, for example the `Paragraph` component is used to render paragraphs in the app and a `Header` component is used to render a `header` on all screens.
- - `common/`: Features that have integral role in app functionality, for example, notesnook core is initialized here.
+- `app/`: Includes all the app code other than the native part. All JS-only dependencies are installed here.
+ - `components/`: Each component serves a specific purpose in the app UI. For example, the `Paragraph` component is used to render paragraphs in the app, and a `Header` component is used to render a `header` on all screens.
+ - `common/`: Features that are integral to the app's functionality. For example, the notesnook core is initialized here.
- `hooks/`: Hooks for different app logic
- - `navigation/`: Includes app navigation specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
+ - `navigation/`: Includes app navigation-specific code. Here the app navigation, editor & side menu are rendered side by side in fluid tabs.
- `screens`: Navigator screens.
- - `services`: Parts of code that do a specific function, for example, the `sync` service is responsibe for running Sync from anywhere in the app.
- - `stores`: We use `zustand` for global state management in the app. There are multiple stores that provide the state for different parts of the app.
- - `utils`: General purpose stuff such as constant values, utility functions etc.
+ - `services`: Parts of code that do a specific function. For example, the `sync` service runs Sync from anywhere in the app.
+ - `stores`: We use `zustand` for global state management in the app. Multiple stores provide the state for different parts of the app.
+ - `utils`: General purpose stuff such as constant values, utility functions, etc.
There are several other folders at the root:
@@ -115,11 +115,11 @@ There are several other folders at the root:
### Running the tests
-When you are done making the required changes, you will need to run the tests to make sure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
+When you are done making the required changes, you must run the tests to ensure you didn't break anything. We use Detox as the testing framework & the tests can be started as follows:
### Android
-To run the tests on android, you will need to create an emulator device on your system:
+To run the tests on Android, you will need to create an emulator device on your system:
```
$ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --package "system-images;android-31;default;x86_64"
@@ -127,13 +127,13 @@ $ANDROID_HOME/tools/bin/avdmanager create avd -n Pixel_5_API_31 -d pixel --packa
If you face problems, follow the detailed guide in [Detox documentation](https://wix.github.io/Detox/docs/introduction/android-dev-env). Keep the emulator name set to `Pixel_5_API_31`.
-Once you have created an emulator device, build the android apks
+Once you have created an emulator device, build the Android apks:
```
npm run build:android
```
-Finally run the tests
+Finally, run the tests:
```
npm run test:android
@@ -141,9 +141,9 @@ npm run test:android
### iOS
-To run e2e tests on iOS simulator, you must be on a Mac with XCode installed.
+To run e2e tests on the iOS simulator, you must be on a Mac with XCode installed.
-First install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils)
+First, install [AppleSimulatorUtils](https://github.com/wix/AppleSimulatorUtils):
```
brew tap wix/brew
@@ -156,7 +156,7 @@ Now build the iOS app for testing:
npm run build:ios
```
-Finally run the tests:
+Finally, run the tests:
```
npm run test:ios
diff --git a/apps/mobile/app/components/list/index.js b/apps/mobile/app/components/list/index.js
index 495f8b323..28c3fc7bf 100644
--- a/apps/mobile/app/components/list/index.js
+++ b/apps/mobile/app/components/list/index.js
@@ -218,12 +218,14 @@ const List = ({
}
/>
-
+ {listData ? (
+
+ ) : null}
>
);
};
diff --git a/apps/mobile/app/components/sheets/add-notebook/index.js b/apps/mobile/app/components/sheets/add-notebook/index.js
index 596b360aa..898ae5e75 100644
--- a/apps/mobile/app/components/sheets/add-notebook/index.js
+++ b/apps/mobile/app/components/sheets/add-notebook/index.js
@@ -25,22 +25,22 @@ import {
TouchableOpacity,
View
} from "react-native";
+import { FlatList } from "react-native-actions-sheet";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
-import { presentSheet, ToastEvent } from "../../../services/event-manager";
+import { ToastEvent, presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useRelationStore } from "../../../stores/use-relation-store";
-import { ph, pv, SIZE } from "../../../utils/size";
+import { SIZE, ph, pv } from "../../../utils/size";
import { sleep } from "../../../utils/time";
-import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
+import Heading from "../../ui/typography/heading";
import { MoveNotes } from "../move-notes/movenote";
-import { FlatList } from "react-native-actions-sheet";
let refs = [];
export class AddNotebookSheet extends React.Component {
@@ -245,13 +245,34 @@ export class AddNotebookSheet extends React.Component {
willFocus && this.topicInputRef.current?.focus();
};
+ renderTopicItem = ({ item, index }) => (
+ {
+ this.prevIndex = index;
+ this.prevItem = item;
+ this.topicInputRef.current?.setNativeProps({
+ text: item
+ });
+ this.topicInputRef.current?.focus();
+ this.currentInputValue = item;
+ this.setState({
+ editTopic: true
+ });
+ }}
+ onDelete={this.onDelete}
+ index={index}
+ colors={this.props.colors}
+ />
+ );
+
render() {
const { colors } = this.props;
const { topics, topicInputFocused, notebook } = this.state;
return (
-
-
+
+
+
+ {notebook && notebook.dateCreated
+ ? "Edit Notebook"
+ : "New Notebook"}
+
+
+
+
+
(this.titleRef = ref)}
@@ -344,48 +382,11 @@ export class AddNotebookSheet extends React.Component {
keyExtractor={(item, index) => item + index.toString()}
keyboardShouldPersistTaps="always"
keyboardDismissMode="interactive"
- ListFooterComponent={}
- renderItem={({ item, index }) => (
- {
- this.prevIndex = index;
- this.prevItem = item;
- this.topicInputRef.current?.setNativeProps({
- text: item
- });
- this.topicInputRef.current?.focus();
- this.currentInputValue = item;
- this.setState({
- editTopic: true
- });
- }}
- onDelete={this.onDelete}
- index={index}
- colors={colors}
- />
- )}
- />
-
-
}
- type="accent"
- onPress={this.addNewNotebook}
+ renderItem={this.renderTopicItem}
/>
- {/*
- {Platform.OS === 'ios' && (
-
- )} */}
);
}
diff --git a/apps/mobile/app/components/sheets/reminder/index.tsx b/apps/mobile/app/components/sheets/reminder/index.tsx
index 109f21fbd..46951869a 100644
--- a/apps/mobile/app/components/sheets/reminder/index.tsx
+++ b/apps/mobile/app/components/sheets/reminder/index.tsx
@@ -21,9 +21,9 @@ import { Platform, TextInput, View } from "react-native";
import { ActionSheetRef, ScrollView } from "react-native-actions-sheet";
import DateTimePickerModal from "react-native-modal-datetime-picker";
import {
- presentSheet,
PresentSheetOptions,
- ToastEvent
+ ToastEvent,
+ presentSheet
} from "../../../services/event-manager";
import { useThemeStore } from "../../../stores/use-theme-store";
import { SIZE } from "../../../utils/size";
@@ -33,6 +33,7 @@ import Input from "../../ui/input";
import dayjs from "dayjs";
import DatePicker from "react-native-date-picker";
import { db } from "../../../common/database";
+import { DDS } from "../../../services/device-detection";
import Navigation from "../../../services/navigation";
import Notifications, { Reminder } from "../../../services/notifications";
import PremiumService from "../../../services/premium";
@@ -41,6 +42,7 @@ import { useRelationStore } from "../../../stores/use-relation-store";
import { NoteType } from "../../../utils/types";
import { Dialog } from "../../dialog";
import { ReminderTime } from "../../ui/reminder-time";
+import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
type ReminderSheetProps = {
@@ -219,35 +221,57 @@ export default function ReminderSheet({
return (
+
+ Set reminder
+
+
-
+
(title.current = text)}
- containerStyle={{ borderWidth: 0, borderBottomWidth: 1 }}
+ wrapperStyle={{
+ marginTop: 10
+ }}
/>
(details.current = text)}
containerStyle={{
- borderWidth: 0,
- borderBottomWidth: 1,
- maxHeight: 80,
- marginTop: 10
+ maxHeight: 80
}}
multiline
textAlignVertical="top"
inputStyle={{
- height: 80
+ minHeight: 80,
+ paddingVertical: 12
}}
height={80}
wrapperStyle={{
@@ -432,6 +456,7 @@ export default function ReminderSheet({
fadeToColor={colors.bg}
theme={colors.night ? "dark" : "light"}
is24hourSource="locale"
+ androidVariant="nativeAndroid"
mode={reminderMode === ReminderModes.Repeat ? "time" : "datetime"}
/>
@@ -548,15 +573,6 @@ export default function ReminderSheet({
alignSelf: "flex-start"
}}
/>
-
);
@@ -570,6 +586,7 @@ ReminderSheet.present = (
presentSheet({
context: isSheet ? "local" : undefined,
enableGesturesInScrollView: true,
+ noBottomPadding: true,
component: (ref, close, update) => (
{
}
};
export const SIZE = {
- xxs: normalize(10.5) * scale.fontScale,
- xs: normalize(12) * scale.fontScale,
- sm: normalize(14.5) * scale.fontScale,
- md: normalize(16) * scale.fontScale,
+ xxs: normalize(11) * scale.fontScale,
+ xs: normalize(12.5) * scale.fontScale,
+ sm: normalize(15) * scale.fontScale,
+ md: normalize(16.5) * scale.fontScale,
lg: normalize(22) * scale.fontScale,
xl: normalize(24) * scale.fontScale,
xxl: normalize(28) * scale.fontScale,
@@ -91,10 +91,10 @@ export const SIZE = {
};
export function updateSize() {
- SIZE.xxs = normalize(10.5) * scale.fontScale;
- SIZE.xs = normalize(12) * scale.fontScale;
- SIZE.sm = normalize(14.5) * scale.fontScale;
- SIZE.md = normalize(16) * scale.fontScale;
+ SIZE.xxs = normalize(11) * scale.fontScale;
+ SIZE.xs = normalize(12.5) * scale.fontScale;
+ SIZE.sm = normalize(15) * scale.fontScale;
+ SIZE.md = normalize(16.5) * scale.fontScale;
SIZE.lg = normalize(22) * scale.fontScale;
SIZE.xl = normalize(24) * scale.fontScale;
SIZE.xxl = normalize(28) * scale.fontScale;
diff --git a/apps/mobile/share/search.js b/apps/mobile/share/search.js
index 83b35167f..1368e2d4f 100644
--- a/apps/mobile/share/search.js
+++ b/apps/mobile/share/search.js
@@ -335,7 +335,13 @@ export const Search = ({ close, getKeyboardHeight, quicknote, mode }) => {
renderItem={renderItem}
estimatedItemSize={50}
ListHeaderComponent={
- searchResults.length === 0 && searchKeyword.current ? (
+ mode === "selectTags" &&
+ (searchResults.length === 0 ||
+ (searchKeyword.current &&
+ searchKeyword.current.length > 0 &&
+ !searchResults.find(
+ (item) => item.title === searchKeyword.current
+ ))) ? (
**Before you start, it is recommended that you read [the contributing guidelines](/CONTRIBUTING.md).**
### Setting up the development environment
@@ -66,11 +66,11 @@ npx serve apps/web/build
## Developer guide
-> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript so if you can help with that, it'd be great!
+> This project is in a transition state between Javascript & Typescript. We are gradually porting everything over to Typescript, so if you can help with that, it'd be great!
### The tech stack
-We try to keep the stack as lean as possible
+We try to keep the stack as lean as possible:
1. React v17: UI framework
2. Typescript/Javascript: The logical side of the app
@@ -82,21 +82,21 @@ We try to keep the stack as lean as possible
### Project structure
-1. `src/`: 99% of the source code lives here & this is also where you'll be spending most of your time.
- 1. `index.tsx`: **the app entrypoint** responsible for loading the appropriate view based on the current route.
- 2. `app.js`: **the default route** that contains the whole note taking experience (notes list, navigation, editor etc.)
- 3. `views/`: Contains **all the views** including views for login, settings, notes, notebooks & topics.
- 4. `components/`: All the **reusuable UI components** are here (e.g. button, editor, etc.)
- 5. `stores/`: Contains the glue code & **logic for all the UI interaction**. For example, when you pin a note the `src/stores/note-store.js` is responsible for everything including refreshing the list to reflect the changes.
- 6. `navigation/`: All the **routing & navigation** logic lives here. The app uses 2 kinds of routers:
- 1. `routes.js`: This contains all the main routes like `/notes`, `/notebooks` with information on what to render when user goes to a particular route.
- 2. `hash-routes.js`: The hash routes are used for temporary navigation like opening dialogs, opening a note. These look like `#/notes/6307bbd65d5d5d5cb86f6f74/edit`.
- 7. `interfaces/`: This is where the **platform specific storage & encryption logic** lives. These interface implementations are used by the `@notesnook/core` to provide capabilities such as persistence & encryption.
- 8. `hooks/`: Contains all the **general purpose React hooks**
- 9. `utils/`: These are **general-purpose utilities** for performing various tasks such as downloading files, storing configuration etc.
+1. `src/`: 99% of the source code lives here & this is also where you'll spend most of your time.
+ 1. `index.tsx`: **the app entry point** responsible for loading the appropriate view based on the current route.
+ 2. `app.js`: **the default route** that contains the whole note-taking experience (notes list, navigation, editor, etc.)
+ 3. `views/`: Contains **all the views**, including views for login, settings, notes, notebooks & topics.
+ 4. `components/`: All the **reusable UI components** are here (e.g., button, editor, etc.)
+ 5. `stores/`: Contains the glue code & **logic for all the UI interactions**. For example, when you pin a note, the `src/stores/note-store.js` is responsible for everything, including refreshing the list to reflect the changes.
+ 6. `navigation/`: All the **routing & navigation** logic lives here. The app uses two kinds of routers:
+ 1. `routes.js`: This contains all the main routes like `/notes`, `/notebooks` with information on what to render when the user goes to a particular route.
+ 2. `hash-routes.js`: The hash routes are used for temporary navigation, like opening dialogs or opening a note. These look like `#/notes/6307bbd65d5d5d5cb86f6f74/edit`.
+ 7. `interfaces/`: This is where the **platform-specific storage & encryption logic** lives. These interface implementations are used by the `@notesnook/core` to provide capabilities such as persistence & encryption.
+ 8. `hooks/`: Contains all the **general-purpose React hooks**
+ 9. `utils/`: These are **general-purpose utilities** for performing various tasks such as downloading files, storing configuration, etc.
10. `common/`: This directory contains **the shared logic between the whole app**. For example, this is where the database is instantiated for use throughout the app.
- 11. `commands/`: These are **commands used by the desktop app** for things like checking for updates, storing backups etc.
-2. `desktop/`: The Electron layer for **the desktop app lives here**. (This should be moved outside into it's own project).
+ 11. `commands/`: These are **commands the desktop app uses** for things like checking for updates, storing backups etc.
+2. `desktop/`: The Electron layer for **the desktop app lives here**. (This should be moved outside into its own project).
### Running the tests
diff --git a/apps/web/src/common/dialog-controller.tsx b/apps/web/src/common/dialog-controller.tsx
index ea2909958..ecf81b082 100644
--- a/apps/web/src/common/dialog-controller.tsx
+++ b/apps/web/src/common/dialog-controller.tsx
@@ -94,7 +94,9 @@ export function showAddNotebookDialog() {
isOpen={true}
onDone={async (nb: Record) => {
// add the notebook to db
- await db.notebooks?.add({ ...nb });
+ const notebook = await db.notebooks?.add({ ...nb });
+ if (!notebook) return perform(false);
+
notebookStore.refresh();
showToast("success", "Notebook added successfully!");
@@ -209,9 +211,8 @@ export function showError(title: string, message: string) {
export function showMultiDeleteConfirmation(length: number) {
return confirm({
title: `Delete ${length} items?`,
- message: `These items will be **kept in your Trash for ${
- db.settings?.getTrashCleanupInterval() || 7
- } days** after which they will be permanently deleted.`,
+ message: `These items will be **kept in your Trash for ${db.settings?.getTrashCleanupInterval() || 7
+ } days** after which they will be permanently deleted.`,
positiveButtonText: "Yes",
negativeButtonText: "No"
});
diff --git a/apps/web/src/hooks/use-database.js b/apps/web/src/hooks/use-database.ts
similarity index 93%
rename from apps/web/src/hooks/use-database.js
rename to apps/web/src/hooks/use-database.ts
index e555c9327..ab9dd4ccf 100644
--- a/apps/web/src/hooks/use-database.js
+++ b/apps/web/src/hooks/use-database.ts
@@ -20,6 +20,7 @@ along with this program. If not, see .
import { useEffect, useState } from "react";
import { initializeDatabase } from "../common/db";
import "../utils/analytics";
+import "../app.css";
if (process.env.NODE_ENV === "production") {
console.log = () => {};
@@ -28,14 +29,13 @@ if (process.env.NODE_ENV === "production") {
const memory = {
isAppLoaded: false
};
-export default function useDatabase(persistence) {
+export default function useDatabase(persistence?: "db" | "memory") {
const [isAppLoaded, setIsAppLoaded] = useState(memory.isAppLoaded);
useEffect(() => {
if (memory.isAppLoaded) return;
(async () => {
- await import("../app.css");
await initializeDatabase(persistence);
setIsAppLoaded(true);
memory.isAppLoaded = true;
diff --git a/apps/web/src/hooks/use-media-query.js b/apps/web/src/hooks/use-media-query.ts
similarity index 97%
rename from apps/web/src/hooks/use-media-query.js
rename to apps/web/src/hooks/use-media-query.ts
index 27ed19660..e3ba08637 100644
--- a/apps/web/src/hooks/use-media-query.js
+++ b/apps/web/src/hooks/use-media-query.ts
@@ -32,7 +32,7 @@ const errorMessage =
* Returns the validity state of the given media query.
*
*/
-const useMediaQuery = (mediaQuery) => {
+const useMediaQuery = (mediaQuery: string) => {
if (!window.matchMedia) {
// eslint-disable-next-line no-console
console.warn(errorMessage);
diff --git a/packages/core/collections/notes.js b/packages/core/collections/notes.js
index 71d28555f..c04fdf66c 100644
--- a/packages/core/collections/notes.js
+++ b/packages/core/collections/notes.js
@@ -59,7 +59,7 @@ export default class Notes extends Collection {
if (
remoteNote.deleted &&
remoteNote.deleteReason !== "localOnly" &&
- !localNote.localOnly
+ (!localNote || !localNote.localOnly)
)
return await this._collection.addItem(remoteNote);
diff --git a/packages/core/collections/tags.js b/packages/core/collections/tags.js
index eb463dd36..71049c257 100644
--- a/packages/core/collections/tags.js
+++ b/packages/core/collections/tags.js
@@ -79,9 +79,6 @@ export default class Tags extends Collection {
return;
}
- newName = this.sanitize(newName);
- if (!newName) throw new Error("Tag title cannot be empty.");
-
await this._db.settings.setAlias(tagId, newName);
await this._collection.addItem({ ...tag, alias: newName });
}
diff --git a/packages/editor/src/extensions/code-block/code-block.ts b/packages/editor/src/extensions/code-block/code-block.ts
index a3e2d0eca..cee1d2fd3 100644
--- a/packages/editor/src/extensions/code-block/code-block.ts
+++ b/packages/editor/src/extensions/code-block/code-block.ts
@@ -127,7 +127,7 @@ export const CodeBlock = Node.create({
id: {
default: undefined,
rendered: false,
- parseHTML: () => `codeblock-${nanoid(12)}`
+ parseHTML: () => createCodeblockId()
},
caretPosition: {
default: undefined,
@@ -227,13 +227,18 @@ export const CodeBlock = Node.create({
setCodeBlock:
(attributes) =>
({ commands }) => {
- return commands.setNode(this.name, attributes);
+ return commands.setNode(this.name, {
+ ...attributes,
+ id: createCodeblockId()
+ });
},
toggleCodeBlock:
(attributes) =>
({ commands }) => {
- console.log("TOGGLING!");
- return commands.toggleNode(this.name, "paragraph", attributes);
+ return commands.toggleNode(this.name, "paragraph", {
+ ...attributes,
+ id: createCodeblockId()
+ });
},
changeCodeBlockIndentation:
(options) =>
@@ -477,14 +482,16 @@ export const CodeBlock = Node.create({
find: backtickInputRegex,
type: this.type,
getAttributes: (match) => ({
- language: match[1]
+ language: match[1],
+ id: createCodeblockId()
})
}),
textblockTypeInputRule({
find: tildeInputRegex,
type: this.type,
getAttributes: (match) => ({
- language: match[1]
+ language: match[1],
+ id: createCodeblockId()
})
})
];
@@ -520,7 +527,7 @@ export const CodeBlock = Node.create({
if (isCode && !isInsideCodeBlock) {
tr.replaceSelectionWith(
this.type.create({
- id: `codeblock-${nanoid(12)}`,
+ id: createCodeblockId(),
language,
indentType: indent.type,
indentLength: indent.amount
@@ -805,3 +812,7 @@ export function inferLanguage(node: Element) {
);
return language?.filename;
}
+
+function createCodeblockId() {
+ return `codeblock-${nanoid(12)}`;
+}
diff --git a/packages/editor/src/extensions/code-block/highlighter.ts b/packages/editor/src/extensions/code-block/highlighter.ts
index 9ea582681..f3ee92df0 100644
--- a/packages/editor/src/extensions/code-block/highlighter.ts
+++ b/packages/editor/src/extensions/code-block/highlighter.ts
@@ -125,9 +125,14 @@ export function HighlighterPlugin({
const changedBlocks: Set = new Set();
for (const blockKey in pluginState.languages) {
- if (HIGHLIGHTED_BLOCKS.has(blockKey)) continue;
-
const language = pluginState.languages[blockKey];
+ if (
+ HIGHLIGHTED_BLOCKS.has(blockKey) &&
+ refractor.registered(language)
+ ) {
+ continue;
+ }
+
const languageDefinition = Languages.find(
(l) =>
l.filename === language || l.alias?.some((a) => a === language)
@@ -193,12 +198,21 @@ export function HighlighterPlugin({
});
if (changedBlocks.length > 0) {
const updated: Set = new Set();
+ let hasChanges = false;
+
changedBlocks.forEach((block) => {
if (updated.has(block.pos)) return;
updated.add(block.pos);
const { id, language } = block.node.attrs;
- if (languages[id]) {
+
+ if (
+ !languages[id] ||
+ (language && !refractor.registered(language))
+ ) {
+ languages[id] = language;
+ hasChanges = true;
+ } else {
const newDecorations = getDecorations({
block,
defaultLanguage
@@ -221,12 +235,12 @@ export function HighlighterPlugin({
decorations = decorations.remove(toRemove);
if (toAdd.length > 0)
decorations = decorations.add(tr.doc, toAdd);
- } else {
- languages[id] = language;
+
+ hasChanges = true;
}
});
- if (decorations !== pluginState.decorations) {
+ if (hasChanges) {
return { decorations, languages };
}
}
diff --git a/packages/editor/src/extensions/code-block/tests/__snapshots__/code-block.test.ts.snap b/packages/editor/src/extensions/code-block/tests/__snapshots__/code-block.test.ts.snap
index f79c033eb..bc3b5944a 100644
--- a/packages/editor/src/extensions/code-block/tests/__snapshots__/code-block.test.ts.snap
+++ b/packages/editor/src/extensions/code-block/tests/__snapshots__/code-block.test.ts.snap
@@ -1,5 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[`Adding a new codeblock & changing the language should apply the new highlighting 1`] = `"
functionhello(){}
"`;
+
exports[`codeblocks should get highlighted after pasting 1`] = `"
functionhello(){}
functionhello(){}
"`;
exports[`codeblocks should get highlighted on init 1`] = `"
functionhello(){}
functionhello(){}
"`;
diff --git a/packages/editor/src/extensions/code-block/tests/code-block.test.ts b/packages/editor/src/extensions/code-block/tests/code-block.test.ts
index 90c23face..fae6b90d8 100644
--- a/packages/editor/src/extensions/code-block/tests/code-block.test.ts
+++ b/packages/editor/src/extensions/code-block/tests/code-block.test.ts
@@ -22,6 +22,7 @@ import { expect, test, vi } from "vitest";
import { CodeBlock, inferLanguage } from "../code-block";
import { HighlighterPlugin } from "../highlighter";
import { getChangedNodes } from "../../../utils/prosemirror";
+import { refractor } from "refractor/lib/core";
const CODEBLOCKS_HTML = h("div", [
h("pre", [h("code", ["function hello() { }"])], {
@@ -191,3 +192,37 @@ test("editing code in a highlighted code block should not be too slow", async ()
expect(timings.reduce((a, b) => a + b) / timings.length).toBeLessThan(16);
expect(editorElement.outerHTML).toMatchSnapshot();
});
+
+test("Adding a new codeblock & changing the language should apply the new highlighting", async () => {
+ const editorElement = h("div");
+ const { editor } = createEditor({
+ element: editorElement,
+ extensions: {
+ codeblock: CodeBlock
+ }
+ });
+
+ editor.commands.setCodeBlock();
+ editor.commands.insertContent("function hello() { }");
+
+ editor.commands.updateAttributes(CodeBlock.name, { language: "javascript" });
+
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ expect(editorElement.outerHTML).toMatchSnapshot();
+ expect(refractor.registered("javascript")).toBe(true);
+});
+
+test("Switching codeblock language should register the new language", async () => {
+ const editorElement = h("div");
+ const { editor } = createEditor({
+ element: editorElement,
+ initialContent: CODEBLOCKS_HTML,
+ extensions: {
+ codeblock: CodeBlock
+ }
+ });
+ editor.commands.updateAttributes(CodeBlock.name, { language: "java" });
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ expect(refractor.registered("java")).toBe(true);
+});
diff --git a/packages/editor/src/toolbar/tools/colors.tsx b/packages/editor/src/toolbar/tools/colors.tsx
index 42dc59a2d..e9e891f41 100644
--- a/packages/editor/src/toolbar/tools/colors.tsx
+++ b/packages/editor/src/toolbar/tools/colors.tsx
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import tinycolor from "tinycolor2";
import { PopupWrapper } from "../../components/popup-presenter";
import { config } from "../../utils/config";
@@ -26,6 +26,7 @@ import { ColorPicker, DEFAULT_COLORS } from "../popups/color-picker";
import { useToolbarLocation } from "../stores/toolbar-store";
import { ToolProps } from "../types";
import { getToolbarElement } from "../utils/dom";
+import { PositionOptions } from "../../utils/position";
type ColorToolProps = ToolProps & {
onColorChange: (color?: string) => void;
@@ -49,6 +50,15 @@ export function ColorTool(props: ColorToolProps) {
const [colors, setColors] = useState(
config.get(`custom_${cacheKey}`, []) || []
);
+ const position: PositionOptions = useMemo(() => {
+ return {
+ isTargetAbsolute: true,
+ target: getToolbarElement(),
+ align: isBottom ? "center" : "end",
+ location: isBottom ? "top" : "below",
+ yOffset: 10
+ };
+ }, [isBottom]);
useEffect(() => {
config.set(`custom_${cacheKey}`, colors);
@@ -73,13 +83,7 @@ export function ColorTool(props: ColorToolProps) {
isOpen={isOpen}
id={props.icon}
group={"color"}
- position={{
- isTargetAbsolute: true,
- target: getToolbarElement(),
- align: isBottom ? "center" : "end",
- location: isBottom ? "top" : "below",
- yOffset: 10
- }}
+ position={position}
focusOnRender={false}
blocking={false}
>