Skip to content
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

Slash menu - block properties lookup #356

Merged
merged 8 commits into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ typings/

.DS_Store
.idea/*
.cursorrules
6 changes: 3 additions & 3 deletions blocks/browse/da-browse/da-browse.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LitElement, html, nothing } from 'da-lit';
import { DA_ORIGIN } from '../../shared/constants.js';
import { daFetch } from '../../shared/utils.js';
import { daFetch, getFirstSheet } from '../../shared/utils.js';
import { getNx } from '../../../scripts/utils.js';

// Components
Expand Down Expand Up @@ -61,9 +61,9 @@
if (reFetch) {
const resp = await daFetch(`${DA_ORIGIN}/config/${this.details.owner}/`);
if (!resp.ok) return DEF_EDIT;
const { data, ':type': type } = await resp.json();
const json = await resp.json();

const rows = type === 'multi-sheet' ? data?.data : data;
const rows = getFirstSheet(json);
this.editorConfs = rows.reduce((acc, row) => {
if (row.key === 'editor.path') acc.push(row.value);
return acc;
Expand All @@ -81,7 +81,7 @@

// Sort by length in descending order (longest first)
const matchedConf = matchedConfs.sort((a, b) => b.length - a.length)[0];
console.log(matchedConf);

Check warning on line 84 in blocks/browse/da-browse/da-browse.js

View workflow job for this annotation

GitHub Actions / Running tests (20.x)

Unexpected console statement

return matchedConf.split('=')[1];
}
Expand Down
4 changes: 2 additions & 2 deletions blocks/edit/da-assets/da-assets.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getNx } from '../../../scripts/utils.js';
import { DA_ORIGIN } from '../../shared/constants.js';
import { daFetch } from '../../shared/utils.js';
import { daFetch, getFirstSheet } from '../../shared/utils.js';

const { loadStyle } = await import(`${getNx()}/scripts/nexter.js`);
const { loadIms, handleSignIn } = await import(`${getNx()}/utils/ims.js`);
Expand All @@ -16,7 +16,7 @@ async function fetchValue(path) {
if (!resp.ok) return null;

const json = await resp.json();
const { data } = json[':type'] === 'multi-sheet' ? Object.keys(json)[0] : json;
const data = getFirstSheet(json);
if (!data) return null;

const repoConf = data.find((conf) => conf.key === 'aem.repositoryId');
Expand Down
14 changes: 11 additions & 3 deletions blocks/edit/da-library/helpers/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
import { DOMParser } from 'da-y-wrapper';
import { CON_ORIGIN, getDaAdmin } from '../../../shared/constants.js';
import getPathDetails from '../../../shared/pathDetails.js';
import { daFetch } from '../../../shared/utils.js';
import { daFetch, getFirstSheet } from '../../../shared/utils.js';
import { getRepoId, openAssets } from '../../da-assets/da-assets.js';
import { fetchKeyAutocompleteData } from '../../prose/plugins/slashMenu/keyAutocomplete.js';

const DA_ORIGIN = getDaAdmin();
const REPLACE_CONTENT = '<content>';
Expand Down Expand Up @@ -66,9 +67,9 @@ async function getDaLibraries(owner, repo) {
if (!resp.ok) return [];

const json = await resp.json();
const blockData = (json[':type'] === 'multi-sheet' ? json.data?.data : json.data) || [];

return blockData.reduce((acc, item) => {
const blockData = getFirstSheet(json);
const daLibraries = blockData.reduce((acc, item) => {
const keySplit = item.key.split('-');
if (keySplit[0] === 'library') {
acc.push({
Expand All @@ -79,6 +80,13 @@ async function getDaLibraries(owner, repo) {
}
return acc;
}, []);

const blockJsonUrl = daLibraries.filter((v) => v.name === 'blocks')?.[0]?.sources?.[0];
if (blockJsonUrl) {
fetchKeyAutocompleteData(blockJsonUrl);
}

return daLibraries;
}

async function getAemPlugins(owner, repo) {
Expand Down
62 changes: 62 additions & 0 deletions blocks/edit/prose/plugins/slashMenu/keyAutocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { daFetch, getSheetByIndex } from '../../../../shared/utils.js';

function insertAutocompleteText(state, dispatch, text) {
const { $cursor } = state.selection;

if (!$cursor) return;
const from = $cursor.before();
const to = $cursor.pos;
const tr = state.tr.replaceWith(from, to, state.schema.text(text));
dispatch(tr);
}

export function processKeyData(data) {
const blockMap = new Map();

data?.forEach((item) => {
const itemBlocks = item.blocks.toLowerCase().trim();
const blocks = itemBlocks.split(',').map((block) => block.trim());

const values = item.values.toLowerCase().split('|').map((v) => {
const [label, val] = v.split('=').map((vb) => vb.trim());

return {
title: label,
value: val || label,
command: (state, dispatch) => insertAutocompleteText(state, dispatch, val || label),
class: 'key-autocomplete',
};
});

blocks.forEach((block) => {
if (!blockMap.has(block)) {
blockMap.set(block, new Map());
}
blockMap.get(block).set(item.key, values);
});
});

return blockMap;
}

// getKeyAutocomplete only resolves once setKeyAutocomplete is called
export const [setKeyAutocomplete, getKeyAutocomplete] = (() => {
let resolveData;
const dataPromise = new Promise((resolve) => {
resolveData = resolve;
});

return [
(keyMap) => {
resolveData(keyMap);
},
async () => dataPromise,
];
})();

export async function fetchKeyAutocompleteData(libraryBlockUrl) {
const resp = await daFetch(libraryBlockUrl);
const json = await resp.json();
const keyMap = processKeyData(getSheetByIndex(json, 1));
setKeyAutocomplete(keyMap);
}
42 changes: 31 additions & 11 deletions blocks/edit/prose/plugins/slashMenu/slash-menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import getSheet from '../../../../shared/sheet.js';

const sheet = await getSheet('/blocks/edit/prose/plugins/slashMenu/slash-menu.css');

function isColorCode(str) {
const hexColorRegex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
const rgbColorRegex = /^rgba?\(\s*(\d{1,3}\s*,\s*){2}\d{1,3}(\s*,\s*(0|1|0?\.\d+))?\s*\)$/;

return hexColorRegex.test(str) || rgbColorRegex.test(str) || str?.includes('-gradient(');
}

function createColorSquare(color) {
return html`
<div style="display: flex; align-items: center;">
<div style="width: 20px; height: 20px; background: ${color}; margin: -5px 5px -5px -5px;"></div>
</div>
`;
}

export default class SlashMenu extends LitElement {
static properties = {
items: { type: Array },
Expand Down Expand Up @@ -37,6 +52,7 @@ export default class SlashMenu extends LitElement {
}

hide() {
this.dispatchEvent(new CustomEvent('reset-slashmenu'));
this.visible = false;
this.command = '';
this.selectedIndex = 0;
Expand Down Expand Up @@ -180,17 +196,21 @@ export default class SlashMenu extends LitElement {

return html`
<div class="slash-menu-items">
${filteredItems.map((item, index) => html`
<div
class="slash-menu-item ${index === this.selectedIndex ? 'selected' : ''}"
@click=${() => this.handleItemClick(item)}
>
<span class="slash-menu-icon ${item.class || ''}"></span>
<span class="slash-menu-label">
${item.title}
</span>
</div>
`)}
${filteredItems.map((item, index) => {
const isColor = isColorCode(item.value);
return html`
<div
class="slash-menu-item ${index === this.selectedIndex ? 'selected' : ''}"
@click=${() => this.handleItemClick(item)}
>
${isColor
? createColorSquare(item.value)
: html`<span class="slash-menu-icon ${item.class || ''}"></span>`}
<span class="slash-menu-label">
${item.title}
</span>
</div>`;
})}
</div>
`;
}
Expand Down
95 changes: 86 additions & 9 deletions blocks/edit/prose/plugins/slashMenu/slashMenu.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable max-len */
import { Plugin, PluginKey } from 'da-y-wrapper';
import { getKeyAutocomplete } from './keyAutocomplete.js';
import menuItems from './slashMenuItems.js';
import './slash-menu.js';

Expand All @@ -22,6 +23,13 @@ class SlashMenuView {
this.menu.addEventListener('item-selected', (e) => {
this.selectItem(e.detail);
});

this.menu.addEventListener('reset-slashmenu', () => {
// reset menu to default items
this.menu.items = menuItems;
this.menu.left = 0;
this.menu.top = 0;
});
}

update(view) {
Expand All @@ -33,13 +41,13 @@ class SlashMenuView {
const { $cursor } = state.selection;

if (!$cursor) {
this.menu.hide();
this.hide();
return;
}

const textBefore = $cursor.parent.textContent.slice(0, $cursor.parentOffset);
if (!textBefore?.startsWith('/')) {
if (this.menu.visible) this.menu.hide();
if (this.menu.visible) this.hide();
return;
}

Expand All @@ -60,7 +68,7 @@ class SlashMenuView {
this.menu.command = match[1] || '';
} else if (this.menu.visible) {
this.menu.command = '';
this.menu.hide();
this.hide();
}
}

Expand All @@ -81,45 +89,114 @@ class SlashMenuView {
dispatch(tr);
item.command(newState, dispatch, argument);

this.menu.hide();
this.hide();
}

handleKeyDown(event) {
return this.menu.handleKeyDown(event);
}

hide() {
this.menu.hide();
}

destroy() {
this.menu.remove();
}
}

// Get the table name if the cursor is in a table cell
const getTableName = ($cursor) => {
const { depth } = $cursor;
let tableCellDepth = -1;

// Search up the tree for a table cell
for (let d = depth; d > 0; d -= 1) {
const node = $cursor.node(d);
if (node.type.name === 'table_cell') {
tableCellDepth = d;
break;
}
}

if (tableCellDepth === -1) return false; // not in a table cell

// Get the row node and cell index
const rowDepth = tableCellDepth - 1;
const tableDepth = rowDepth - 1;
const table = $cursor.node(tableDepth);
const firstRow = table.child(0);
const cellIndex = $cursor.index(tableCellDepth - 1);
const row = $cursor.node(rowDepth);

// Only proceed if we're in the second column
if (!(row.childCount > 1 && cellIndex === 1)) return false;

const firstRowContent = firstRow.child(0).textContent;
const tableNameMatch = firstRowContent.match(/^([a-zA-Z0-9_-]+)(?:\s*\([^)]*\))?$/);

const currentRowFirstColContent = row.child(0).textContent;

if (tableNameMatch) {
return {
tableName: tableNameMatch[1],
keyValue: currentRowFirstColContent,
};
}

return false;
};

export default function slashMenu() {
let pluginView = null;

// Start fetching data immediately
getKeyAutocomplete().then((data) => {
if (pluginView?.view) {
const tr = pluginView.view.state.tr.setMeta(slashMenuKey, { autocompleteData: data });
pluginView.view.dispatch(tr);
}
});

return new Plugin({
key: slashMenuKey,
state: {
init() {
return { showSlashMenu: false };
return {
showSlashMenu: false,
autocompleteData: null,
};
},
apply(tr, value) {
const meta = tr.getMeta(slashMenuKey);
if (meta !== undefined) {
return { showSlashMenu: meta };
return { ...value, ...meta };
}
return value;
},
},
props: {
handleKeyDown(editorView, event) {
const { state } = editorView;
const pluginState = slashMenuKey.getState(state);

if (event.key === '/') {
const { $cursor } = state.selection;

// Only show menu if we're at the start of an empty line
if ($cursor && $cursor.parentOffset === 0 && $cursor.parent.textContent === '') {
const tr = state.tr.setMeta(slashMenuKey, true);
// Check if we're at start of empty line
if ($cursor?.parentOffset === 0 && $cursor?.parent?.textContent === '') {
const { tableName, keyValue } = getTableName($cursor);
if (tableName) {
const keyData = pluginState.autocompleteData?.get(tableName);
if (keyData) {
const values = keyData.get(keyValue);
if (values) {
pluginView.menu.items = values;
}
}
}

const tr = state.tr.setMeta(slashMenuKey, { showSlashMenu: true });
editorView.dispatch(tr);
return false;
}
Expand Down
9 changes: 9 additions & 0 deletions blocks/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,12 @@ export async function saveToDa({ path, formData, blob, props, preview = false })
if (!preview) return undefined;
return aemPreview(path, 'preview');
}

export const getSheetByIndex = (json, index = 0) => {
if (json[':type'] !== 'multi-sheet') {
return json.data;
}
return json[Object.keys(json)[index]]?.data;
};

export const getFirstSheet = (json) => getSheetByIndex(json, 0);
Loading