Skip to content

Commit e5ba033

Browse files
xdanclaude
andcommitted
feat(storage): configurable AsyncStorage provider + fix LocalStorageProvider.delete
- AsyncStorage.makeStorage now accepts a third `options` argument with a `defaultProvider` field ('local' | 'memory' | custom IAsyncStorage) that overrides the backing provider; default behaviour (IndexedDB + memory fallback) is unchanged. Exposed on the editor as the `asyncStorage` config option so jodit.asyncStorage can target localStorage, memory, or a custom backend without subclassing. - Fix: LocalStorageProvider.delete(key) removed the entire scope (behaved like clear); it now drops only the requested key and keeps the rest. - Expand storage/button/group/tooltip READMEs; add tests and changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent add1586 commit e5ba033

13 files changed

Lines changed: 629 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
> - :house: [Internal]
1010
> - :nail_care: [Polish]
1111

12+
## Unreleased
13+
14+
#### :rocket: New Feature
15+
16+
- **AsyncStorage**: `AsyncStorage.makeStorage(persistentOrStrategy, suffix, options?)` now accepts a third `options` argument with a `defaultProvider` field that overrides which provider backs the storage — `'local'` (localStorage), `'memory'`, or a custom `IAsyncStorage` implementation. When omitted the behaviour is unchanged (persistent IndexedDB with an in-memory fallback). The same option is exposed on the editor as the `asyncStorage` config option (`Jodit.make('#editor', { asyncStorage: { defaultProvider: 'local' } })`), so `jodit.asyncStorage` can be pointed at localStorage, memory, or your own backend without subclassing.
17+
18+
#### :bug: Bug Fix
19+
20+
- **Storage**: `LocalStorageProvider.delete(key)` removed the entire storage scope (every key sharing the same `rootKey`/suffix) instead of just the requested key — `delete` behaved identically to `clear`. It now reads the JSON blob, drops only that key and writes the rest back. Affects `Jodit.modules.Storage`/`buffer`/`storage` and the `@persistent` decorator when a single key is deleted.
21+
1222
## 4.12.42
1323

1424
#### :bug: Bug Fix

src/config.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
Attributes,
3030
ButtonsOption,
3131
Controls,
32+
IAsyncStorageOptions,
3233
IControlType,
3334
IDictionary,
3435
IExtraPlugin,
@@ -414,6 +415,28 @@ class Config implements IViewOptions {
414415
*/
415416
saveModeInStorage: boolean = false;
416417

418+
/**
419+
* Configure the provider that backs {@link IViewBased.asyncStorage}.
420+
*
421+
* By default the editor's `asyncStorage` uses persistent `IndexedDB` (with an
422+
* in-memory fallback when it is unavailable). Set `defaultProvider` to override it:
423+
* - `'local'` — persist in `localStorage`;
424+
* - `'memory'` — keep everything in memory (nothing survives a reload);
425+
* - a custom {@link IAsyncStorage} implementation — plug in your own backend.
426+
*
427+
* ```javascript
428+
* Jodit.make('#editor', {
429+
* asyncStorage: { defaultProvider: 'local' }
430+
* });
431+
*
432+
* // or a fully custom backend
433+
* Jodit.make('#editor', {
434+
* asyncStorage: { defaultProvider: myAsyncStorage }
435+
* });
436+
* ```
437+
*/
438+
asyncStorage: IAsyncStorageOptions = {};
439+
417440
/**
418441
* Class name that can be appended to the editable area
419442
*

src/core/storage/README.md

Lines changed: 225 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,237 @@
11
---
22
title: Storage Module
3-
description: A persistent storage module that saves data to the user's local storage, falling back to an in-memory provider when persistent storage is not permitted.
4-
keywords: jodit, storage, persistent storage, local storage, memory storage, data persistence
3+
description: Persistent and asynchronous key/value storage for Jodit. Saves data to localStorage, sessionStorage or IndexedDB and transparently falls back to an in-memory provider when persistent storage is not permitted.
4+
keywords: jodit, storage, persistent storage, local storage, session storage, indexeddb, memory storage, data persistence, async storage
55
---
66

7-
# Persistent storage
7+
# Storage
88

9-
The module is designed to save information to the user's local storage.
10-
At startup, it is checked whether the user has allowed saving to persistent storage.
9+
The storage module is a small key/value abstraction used across the editor to persist
10+
things like the current editing mode, the file browser view, panel sizes, and any state
11+
saved through the [[persistent]] decorator.
1112

12-
> If not allowed, the module will use the [[MemoryStorageProvider]] strategy.
13+
It ships with two facades and three underlying providers (engines):
14+
15+
| Facade | API | Providers it can pick |
16+
| --- | --- | --- |
17+
| [[Storage]] | **synchronous** | [[LocalStorageProvider]] (local/session), [[MemoryStorageProvider]] |
18+
| [[AsyncStorage]] | **Promise-based** | [[IndexedDBProvider]], [[LocalStorageProvider]], [[MemoryStorageProvider]] |
19+
20+
At construction time each facade probes whether the requested persistent engine is actually
21+
usable (e.g. the browser may block `localStorage` in private mode, or `IndexedDB` may be
22+
disabled). If it is not, the facade silently falls back to [[MemoryStorageProvider]], so
23+
`set`/`get` never throw — they just stop surviving reloads.
24+
25+
## Using it from a Jodit instance
26+
27+
Every `Jodit`/`View` instance exposes three ready-made storages (see `core/view/view.ts`):
1328

1429
```js
1530
const jodit = Jodit.make('#editor');
16-
jodit.storage.set('someKey', 1);
1731

18-
// reload page
32+
// Persistent, synchronous, scoped to this editor instance (suffix = jodit.id)
33+
jodit.storage.set('height', 300);
34+
jodit.storage.get('height'); // 300 (survives page reload)
35+
36+
// Persistent, asynchronous (IndexedDB when available), same scope
37+
await jodit.asyncStorage.set('draft', { html: '<p>hi</p>' });
38+
await jodit.asyncStorage.get('draft'); // { html: '<p>hi</p>' }
39+
40+
// Non-persistent scratch space, shared process-wide (always memory-backed)
41+
jodit.buffer.set('copyFormat', style);
42+
jodit.buffer.get('copyFormat');
43+
```
44+
45+
- `jodit.storage``Storage.makeStorage(true, jodit.id)`, **deprecated** in favour of `asyncStorage`.
46+
- `jodit.asyncStorage``AsyncStorage.makeStorage(true, jodit.id, jodit.o.asyncStorage)`; prefer this for new code.
47+
- `jodit.buffer``Storage.makeStorage()` (no persistence), for transient data.
48+
49+
### Choosing the provider via editor config
50+
51+
The provider that backs `jodit.asyncStorage` can be configured with the `asyncStorage`
52+
editor option. By default it is persistent `IndexedDB` (with a memory fallback); set
53+
`defaultProvider` to override it:
54+
55+
```js
56+
// Persist in localStorage instead of IndexedDB
57+
Jodit.make('#editor', { asyncStorage: { defaultProvider: 'local' } });
58+
59+
// Keep everything in memory (nothing survives a reload)
60+
Jodit.make('#editor', { asyncStorage: { defaultProvider: 'memory' } });
61+
62+
// Plug in your own backend (any object implementing IAsyncStorage)
63+
Jodit.make('#editor', { asyncStorage: { defaultProvider: myAsyncStorage } });
64+
```
65+
66+
## Using it directly (without a Jodit instance)
67+
68+
Both facades and all providers are exported on `Jodit.modules`, so you can build a standalone
69+
storage without creating an editor. This is exactly how the unit tests exercise the module.
70+
71+
### Synchronous — `Jodit.modules.Storage`
72+
73+
```js
74+
const { Storage } = Jodit.modules;
75+
76+
// makeStorage(persistentOrStrategy = false, suffix?)
77+
const storage = Storage.makeStorage('localStorage', 'myApp');
78+
79+
storage.set('theme', 'dark'); // chainable — returns `this`
80+
storage.get('theme'); // 'dark'
81+
storage.get('missing'); // undefined
82+
storage.exists('theme'); // true
83+
storage.delete('theme');
84+
storage.clear();
85+
```
86+
87+
`persistentOrStrategy` accepts:
88+
89+
| Value | Engine chosen |
90+
| --- | --- |
91+
| `false` *(default)* | [[MemoryStorageProvider]] |
92+
| `true` | [[LocalStorageProvider]] with `localStorage` (memory fallback) |
93+
| `'localStorage'` | [[LocalStorageProvider]] with `localStorage` (memory fallback) |
94+
| `'sessionStorage'` | [[LocalStorageProvider]] with `sessionStorage` (memory fallback) |
95+
96+
### Asynchronous — `Jodit.modules.AsyncStorage`
97+
98+
Same shape, but every method returns a `Promise`. It adds `IndexedDB` support and a `close()`
99+
method (only meaningful for IndexedDB — it releases the DB connection).
100+
101+
```js
102+
const { AsyncStorage } = Jodit.modules;
103+
104+
// makeStorage(persistentOrStrategy = false, suffix?, options?)
105+
const storage = AsyncStorage.makeStorage('indexedDB', 'myApp');
106+
107+
await storage.set('user', { id: 1, name: 'Ann' });
108+
await storage.get('user'); // { id: 1, name: 'Ann' }
109+
await storage.exists('user'); // true
110+
await storage.delete('user');
111+
await storage.clear();
112+
await storage.close(); // release the IndexedDB connection when done
113+
```
114+
115+
#### Overriding the provider — `options.defaultProvider`
116+
117+
The optional third argument lets you force which provider backs the storage, regardless of
118+
the first argument. When omitted the storage behaves exactly as before.
119+
120+
```js
121+
// localStorage instead of the default IndexedDB
122+
AsyncStorage.makeStorage(true, 'myApp', { defaultProvider: 'local' });
123+
124+
// in-memory only
125+
AsyncStorage.makeStorage(true, 'myApp', { defaultProvider: 'memory' });
126+
127+
// your own IAsyncStorage implementation (set/delete/get/exists/clear/close)
128+
AsyncStorage.makeStorage(true, 'myApp', { defaultProvider: myAsyncStorage });
129+
```
130+
131+
| `defaultProvider` | Engine used |
132+
| --- | --- |
133+
| *(omitted)* | Falls back to the first-argument strategy (default: IndexedDB) |
134+
| `'local'` | [[LocalStorageProvider]] with `localStorage` (memory fallback) |
135+
| `'memory'` | [[MemoryStorageProvider]] |
136+
| an `IAsyncStorage` object | Your implementation, used as-is |
137+
138+
A custom provider is still wrapped by the facade, so keys passed to it are namespaced
139+
(`Jodit_` + suffix, camelCased) just like the built-in providers.
140+
141+
`persistentOrStrategy` for `AsyncStorage`:
142+
143+
| Value | Engine chosen |
144+
| --- | --- |
145+
| `false` *(default)* | [[MemoryStorageProvider]] |
146+
| `true` | [[IndexedDBProvider]] (memory fallback if IndexedDB is unavailable) |
147+
| `'indexedDB'` | [[IndexedDBProvider]] (memory fallback) |
148+
| `'localStorage'` | [[LocalStorageProvider]] with `localStorage` |
149+
| `'sessionStorage'` | [[LocalStorageProvider]] with `sessionStorage` |
150+
151+
Because provider selection for IndexedDB is itself asynchronous, the facade wraps a
152+
`Promise<provider>` internally and each call awaits it. You never see that promise — just
153+
`await` the storage methods.
19154

20-
jodit.storage.get('someKey'); // 1
155+
### Using a provider directly
156+
157+
The engines implement `IStorage` / `IAsyncStorage` and can be instantiated on their own when
158+
you want full control over the root key and no `Jodit_`/camelCase key mangling (see caveats
159+
below):
160+
161+
```js
162+
const { LocalStorageProvider, MemoryStorageProvider, IndexedDBProvider } = Jodit.modules;
163+
164+
const local = new LocalStorageProvider('rootKey', 'sessionStorage');
165+
local.set('a', 1);
166+
167+
const mem = new MemoryStorageProvider();
168+
169+
const idb = new IndexedDBProvider('myDb', 'keyValueStore');
170+
await idb.set('a', 1);
171+
// IndexedDBProvider also has extras not on the IStorage interface:
172+
await idb.keys(); // ['a']
173+
await idb.values(); // [1]
174+
await idb.entries(); // [['a', 1]]
175+
await idb.close();
21176
```
177+
178+
## Feature detection
179+
180+
Two helpers are exported so you can check availability before choosing a strategy:
181+
182+
```js
183+
// Synchronous — probes localStorage / sessionStorage by writing a temp key.
184+
Jodit.modules.canUsePersistentStorage('localStorage'); // boolean
185+
Jodit.modules.canUsePersistentStorage('sessionStorage'); // boolean
186+
187+
// Asynchronous — opens a throwaway IndexedDB database. Result is cached.
188+
await Jodit.modules.canUseIndexedDB(); // Promise<boolean>
189+
Jodit.modules.clearUseIndexedDBCache(); // reset the cached result (used in tests)
190+
```
191+
192+
## Namespacing: prefix, suffix and key mangling
193+
194+
- Every facade prefixes stored keys with `Jodit_` (exported as `StorageKey`).
195+
- The optional `suffix` passed to `makeStorage` is appended to that prefix, letting several
196+
independent storages coexist (`Jodit.make` uses the editor's `id` as the suffix so two
197+
editors on the same page don't clobber each other's data).
198+
- Keys are run through `camelCase`, so `storage.set('my-key')` and `storage.set('myKey')`
199+
address **the same** entry. Prefer plain identifiers for keys.
200+
201+
```js
202+
Storage.makeStorage(true, 'app1').set('key', 'a');
203+
Storage.makeStorage(true, 'app2').set('key', 'b');
204+
// 'app1' and 'app2' are isolated: each 'key' keeps its own value.
205+
```
206+
207+
## Supported value types
208+
209+
Anything JSON-serialisable: `string`, `number`, `boolean`, `null`, plain objects and arrays
210+
(`StorageValueType`). `LocalStorageProvider` and `IndexedDBProvider` round-trip values through
211+
serialisation, so don't store class instances, functions, `Date`, `undefined`, etc. and expect
212+
them back intact. `MemoryStorageProvider` keeps the original reference in a `Map`.
213+
214+
## Caveats
215+
216+
- **One scope is one JSON blob.** All keys of a `LocalStorageProvider` instance live inside a
217+
single JSON object under one `rootKey`. `delete(key)` removes just that key (the rest of the
218+
scope is preserved); `clear()` drops the whole blob.
219+
- **Failures are swallowed.** Providers wrap storage access in `try/catch` and fall back
220+
silently. A blocked `localStorage`, quota error, or private-mode restriction turns into a
221+
no-op / memory fallback rather than an exception.
222+
- **`asyncStorage.close()`** should be called when you're finished with an IndexedDB-backed
223+
storage to release the connection; the editor does this automatically on `beforeDestruct`.
224+
225+
## API surface
226+
227+
`IStorage<T>` (sync):
228+
229+
```ts
230+
set(key: string, value: T): this;
231+
delete(key: string): this;
232+
get<R = T>(key: string): R | void;
233+
exists(key: string): boolean;
234+
clear(): this;
235+
```
236+
237+
`IAsyncStorage<T>` (async) — same methods returning `Promise`, plus `close(): Promise<void>`.

0 commit comments

Comments
 (0)