-
Notifications
You must be signed in to change notification settings - Fork 65
feat(ew): add support for blocks search in slash menu #1084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| const BLOCK_ICON = 'tableadd'; | ||
|
|
||
| export function buildBlockEntries(resolvedBlocks) { | ||
| const entries = []; | ||
| (resolvedBlocks || []).forEach((block) => { | ||
| (block.variants || []).forEach((variant) => { | ||
| entries.push({ | ||
| id: `block:${entries.length}`, | ||
| blockName: block.name, | ||
| variantName: variant.name, | ||
| variants: variant.variants, | ||
| tags: variant.tags, | ||
| description: variant.description, | ||
| dom: variant.dom, | ||
| }); | ||
| }); | ||
| }); | ||
| return entries; | ||
| } | ||
|
|
||
| function deriveDisplay(entry) { | ||
| const name = entry.variantName || entry.blockName || ''; | ||
| if (entry.variants) return { label: name, description: entry.variants }; | ||
| const match = name.match(/^(.*\S)\s*\(([^)]+)\)\s*$/); | ||
| if (match) return { label: match[1].trim(), description: match[2].trim() }; | ||
| return { label: name, description: undefined }; | ||
| } | ||
|
|
||
| export function matchBlockEntries(entries, query) { | ||
| const terms = (query || '').toLowerCase().split(/\s+/).filter(Boolean); | ||
| if (!terms.length) return []; | ||
|
|
||
| return (entries || []) | ||
| .map((entry, index) => { | ||
| const { label, description } = deriveDisplay(entry); | ||
| const haystack = [entry.blockName, entry.variantName, entry.variants, entry.tags] | ||
| .filter(Boolean).join(' ').toLowerCase(); | ||
| if (!terms.every((term) => haystack.includes(term))) return null; | ||
| const rank = label.toLowerCase().startsWith(terms[0]) ? 0 : 1; | ||
| return { entry, label, description, index, rank }; | ||
| }) | ||
| .filter(Boolean) | ||
| .sort((a, b) => a.rank - b.rank || a.index - b.index) | ||
| .map(({ entry, label, description }) => ({ | ||
| id: entry.id, | ||
| label, | ||
| description: description || undefined, | ||
| icon: BLOCK_ICON, | ||
| })); | ||
| } | ||
|
|
||
| const store = { | ||
| entries: [], | ||
| state: 'idle', // 'idle' | 'loading' | 'ready' | 'empty' | ||
| hasLibrary: false, | ||
| key: null, | ||
| }; | ||
|
|
||
| export function getState() { | ||
| return store.state; | ||
| } | ||
|
|
||
| export function hasLibrary() { | ||
| return store.hasLibrary; | ||
| } | ||
|
|
||
| export function blockItemsForQuery(query) { | ||
| return matchBlockEntries(store.entries, query); | ||
| } | ||
|
|
||
| export function resetBlockLibrary() { | ||
| store.entries = []; | ||
| store.state = 'idle'; | ||
| store.hasLibrary = false; | ||
| store.key = null; | ||
| } | ||
|
|
||
| export async function ingestBlocks(blocks) { | ||
| const resolved = await Promise.all( | ||
| (blocks || []).map(async (block) => ({ | ||
| name: block.name, | ||
| variants: (await block.loadVariants) || [], | ||
| })), | ||
| ); | ||
| store.entries = buildBlockEntries(resolved); | ||
| store.state = store.entries.length ? 'ready' : 'empty'; | ||
| store.hasLibrary = true; | ||
| return store.entries; | ||
| } | ||
|
|
||
| export async function insertBlockItem(view, id) { | ||
| const entry = store.entries.find((e) => e.id === id); | ||
| if (!entry) return; | ||
| const { insertBlock } = await import('../ew-panel-extensions/helpers.js'); | ||
| insertBlock(view, entry.dom); | ||
| } | ||
|
|
||
| export async function prefetchBlockLibrary({ org, site } = {}) { | ||
| if (!org || !site) return; | ||
| const key = `${org}/${site}`; | ||
| if (store.key === key && store.state !== 'idle') return; | ||
| store.key = key; | ||
| store.state = 'loading'; | ||
| try { | ||
| const { loadBlockLibrary } = await import('../ew-panel-extensions/helpers.js'); | ||
| const { ext, blocks } = await loadBlockLibrary(org, site); | ||
| if (!ext) { | ||
| store.entries = []; | ||
| store.hasLibrary = false; | ||
| store.state = 'empty'; | ||
| return; | ||
| } | ||
|
|
||
| store.hasLibrary = true; | ||
| await ingestBlocks(blocks); | ||
| } catch { | ||
| store.state = 'empty'; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,7 +41,7 @@ import { getNx } from '../../../scripts/utils.js'; | |
| import { getAuthToken } from '../../shared/utils.js'; | ||
| import { generateColor, getCollabIdentity } from './utils/collab.js'; | ||
|
|
||
| const { DA_ADMIN, DA_COLLAB } = await import(`${getNx()}/utils/utils.js`); | ||
| const { DA_ADMIN, DA_COLLAB, hashChange } = await import(`${getNx()}/utils/utils.js`); | ||
|
|
||
| function registerErrorHandler(ydoc) { | ||
| ydoc.on('update', () => { | ||
|
|
@@ -66,6 +66,30 @@ function addSyncedListener(wsProvider, canWrite, setEditable) { | |
| wsProvider.on('synced', handleSynced); | ||
| } | ||
|
|
||
| function prefetchBlocksAfterSync(wsProvider, canWrite) { | ||
| if (!canWrite) return; | ||
| const handleSynced = (isSynced) => { | ||
| if (!isSynced) return; | ||
| wsProvider.off('synced', handleSynced); | ||
|
|
||
| let hashState; | ||
| const unsub = hashChange.subscribe((s) => { hashState = s; }); | ||
| unsub(); | ||
| if (!hashState?.org || !hashState?.site) return; | ||
|
|
||
| const kick = async () => { | ||
| const { prefetchBlockLibrary } = await import('../editor-utils/block-slash.js'); | ||
| prefetchBlockLibrary({ org: hashState.org, site: hashState.site }); | ||
| }; | ||
| if (typeof window.requestIdleCallback === 'function') { | ||
| window.requestIdleCallback(kick, { timeout: 2000 }); | ||
| } else { | ||
| setTimeout(kick, 0); | ||
| } | ||
| }; | ||
| wsProvider.on('synced', handleSynced); | ||
| } | ||
|
|
||
| export default async function initProse({ | ||
| path, permissions, setEditable, getToken, | ||
| extraPlugins = [], | ||
|
|
@@ -125,6 +149,7 @@ export default async function initProse({ | |
| }); | ||
|
|
||
| addSyncedListener(wsProvider, canWrite, setEditable); | ||
| prefetchBlocksAfterSync(wsProvider, canWrite); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we could wait until the user types
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about this - but IMO power users are pretty quick if they know what they want from the slash menu. Currently prefetches after the doc is loaded which should be fine? @mhaack thoughts? |
||
| registerErrorHandler(ydoc); | ||
|
|
||
| const yXmlFragment = ydoc.getXmlFragment('prosemirror'); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.