Skip to content

Commit

Permalink
feat: ColumnDataType autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
dineug committed Nov 19, 2023
1 parent e61fd40 commit 71d0bdb
Show file tree
Hide file tree
Showing 26 changed files with 750 additions and 74 deletions.
4 changes: 3 additions & 1 deletion packages/erd-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,17 @@
"@storybook/html": "^7.4.0",
"@storybook/testing-library": "^0.2.0",
"@types/color": "^3.0.3",
"@types/highlight-words-core": "^1.2.3",
"@types/lodash-es": "^4.17.8",
"@types/luxon": "^3.3.1",
"@types/ua-parser-js": "^0.7.37",
"color": "^4.2.3",
"deepmerge": "^4.3.1",
"highlight-words-core": "^1.2.2",
"html-to-image": "^1.11.11",
"lodash-es": "^4.17.21",
"luxon": "^3.3.0",
"nanoid": "^5.0.3",
"rollup-plugin-visualizer": "^5.9.2",
"rxjs": "^7.8.1",
"stats.js": "^0.17.0",
Expand All @@ -79,7 +82,6 @@
"typescript-transform-paths": "^3.4.6",
"typescript": "5.1.6",
"ua-parser-js": "^1.0.36",
"nanoid": "^5.0.3",
"vite-tsconfig-paths": "^4.2.0",
"vite": "^4.4.9"
}
Expand Down
4 changes: 2 additions & 2 deletions packages/erd-editor/src/components/erd-editor/ErdEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import Erd from '@/components/erd/Erd';
import GlobalStyles from '@/components/global-styles/GlobalStyles';
import Theme from '@/components/theme/Theme';
import Toolbar from '@/components/toolbar/Toolbar';
import { DatabaseVendor } from '@/constants/database';
import { TOOLBAR_HEIGHT } from '@/constants/layout';
import { DatabaseVendor } from '@/constants/sql/database';
import { changeViewportAction } from '@/engine/modules/editor/atom.actions';
import { useKeyBindingMap } from '@/hooks/useKeyBindingMap';
import { useUnmounted } from '@/hooks/useUnmounted';
Expand Down Expand Up @@ -87,7 +87,7 @@ const ErdEditor: FC<ErdEditorProps, ErdEditorElement> = (props, ctx) => {
};

const handleFocus = () => {
setTimeout(() => {
window.setTimeout(() => {
if (document.activeElement !== ctx) {
ctx.focus();
}
Expand Down
7 changes: 4 additions & 3 deletions packages/erd-editor/src/components/erd/canvas/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Table from '@/components/erd/canvas/table/Table';
import { Show } from '@/constants/schema';
import { bHas } from '@/utils/bit';
import { query } from '@/utils/collection/query';
import { isHighLevelTable } from '@/utils/validation';

import * as styles from './Canvas.styles';

Expand Down Expand Up @@ -50,16 +51,16 @@ const Canvas: FC<CanvasProps> = (props, ctx) => {
}}
>
${cache(
zoomLevel > 0.7
isHighLevelTable(zoomLevel)
? html`${repeat(
tables,
table => table.id,
table => html`<${Table} table=${table} />`
table => html`<${HighLevelTable} table=${table} />`
)}`
: html`${repeat(
tables,
table => table.id,
table => html`<${HighLevelTable} table=${table} />`
table => html`<${Table} table=${table} />`
)}`
)}
${repeat(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ export const name = css`
word-break: break-all;
color: var(--active);
font-weight: var(--font-weight-bold);
&.isEmptyName {
color: var(--placeholder);
}
`;
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FC, html } from '@dineug/r-html';
import { isEmpty } from 'lodash-es';

import { useAppContext } from '@/components/appContext';
import * as styles from '@/components/erd/canvas/table/Table.styles';
Expand Down Expand Up @@ -52,6 +53,8 @@ const HighLevelTable: FC<HighLevelTableProps> = (props, ctx) => {
const tableWidths = calcTableWidths(table, store.state);
const height = calcTableHeight(table);

const isEmptyName = isEmpty(table.name.trim());

return html`
<div
class=${['table', styles.root]}
Expand All @@ -74,8 +77,15 @@ const HighLevelTable: FC<HighLevelTableProps> = (props, ctx) => {
}}
></div>
</div>
<div class=${['scrollbar', highLevelTableStyle.name, fontSize()]}>
${table.name}
<div
class=${[
'scrollbar',
highLevelTableStyle.name,
fontSize(),
{ isEmptyName },
]}
>
${isEmptyName ? 'unnamed' : table.name}
</div>
</div>
`;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DOMTemplateLiterals, FC, html, Ref, repeat } from '@dineug/r-html';

import { AppContext, useAppContext } from '@/components/appContext';
import ColumnDataType from '@/components/erd/canvas/table/column/column-data-type/ColumnDataType';
import ColumnKey from '@/components/erd/canvas/table/column/column-key/ColumnKey';
import ColumnNotNull from '@/components/erd/canvas/table/column/column-not-null/ColumnNotNull';
import ColumnOption from '@/components/erd/canvas/table/column/column-option/ColumnOption';
Expand Down Expand Up @@ -220,13 +221,13 @@ const Column: FC<ColumnProps> = (props, ctx) => {
handleEdit(FocusType.columnDataType);
}}
>
<${EditInput}
placeholder="dataType"
<${ColumnDataType}
tableId=${column.tableId}
columnId=${column.id}
width=${widthDataType}
value=${column.dataType}
focus=${props.focusDataType}
edit=${props.editDataType}
autofocus=${true}
.onBlur=${handleEditEnd}
.onInput=${(event: InputEvent) => {
handleInput(event, FocusType.columnDataType);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { css } from '@dineug/r-html';

import { INPUT_HEIGHT } from '@/constants/layout';
import { typography } from '@/styles/typography.styles';

export const root = css`
position: relative;
`;

export const hint = css`
position: absolute;
z-index: 1;
top: ${INPUT_HEIGHT}px;
left: 0;
color: var(--foreground);
background-color: var(--table-background);
border: 1px solid var(--table-border);
white-space: nowrap;
${typography.paragraph};
& > div {
display: flex;
align-items: center;
padding: 0 4px;
height: 20px;
cursor: pointer;
}
& > div:hover {
background-color: var(--column-hover);
}
& > div.selected {
background-color: var(--column-select);
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import {
createRef,
FC,
html,
observable,
onMounted,
ref,
repeat,
watch,
} from '@dineug/r-html';
import { arrayHas } from '@dineug/shared';
import { isEmpty } from 'lodash-es';

import { useAppContext } from '@/components/appContext';
import EditInput from '@/components/primitives/edit-input/EditInput';
import HighlightedText from '@/components/primitives/highlighted-text/HighlightedText';
import { DatabaseHintMap, DataTypeHint } from '@/constants/sql/dataType';
import { changeColumnDataTypeAction } from '@/engine/modules/tableColumn/atom.actions';
import { useUnmounted } from '@/hooks/useUnmounted';
import { lastCursorFocus } from '@/utils/focus';

import * as styles from './ColumnDataType.styles';

export type ColumnDataTypeProps = {
tableId: string;
columnId: string;
edit: boolean;
focus: boolean;
width: number;
value: string;
onInput?: (event: InputEvent) => void;
onBlur?: (event: FocusEvent) => void;
};

export interface State {
hints: DataTypeHint[];
index: number;
holdFilter: boolean;
}

const hasArrowKey = arrayHas([
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
]);

const ColumnDataType: FC<ColumnDataTypeProps> = (props, ctx) => {
const app = useAppContext(ctx);
const state = observable<State>({
hints: [],
index: -1,
holdFilter: false,
});
const root = createRef<HTMLDivElement>();
const { addUnsubscribe } = useUnmounted();

const setHints = () => {
if (state.holdFilter) return;

const { store } = app.value;
const { settings } = store.state;
const hints = DatabaseHintMap[settings.database] ?? [];
const value = props.value.trim();

state.index = -1;
state.hints = isEmpty(value)
? []
: hints.filter(
hint => hint.name.toLowerCase().indexOf(value.toLowerCase()) !== -1
);
};

const handleSelectHint = (index: number) => {
const hint = state.hints[index];
if (!hint) return;

const { store } = app.value;
state.holdFilter = true;
store.dispatch(
changeColumnDataTypeAction({
id: props.columnId,
tableId: props.tableId,
value: hint.name,
})
);
};

const handleArrowUp = (event: KeyboardEvent) => {
if (!state.hints.length) return;
event.preventDefault();

const index = state.index - 1;
state.index = index < 0 ? state.hints.length - 1 : index;
};

const handleArrowDown = (event: KeyboardEvent) => {
if (!state.hints.length) return;
event.preventDefault();

const index = state.index + 1;
state.index = index > state.hints.length - 1 ? 0 : index;
};

const handleArrowLeft = (event: KeyboardEvent) => {
state.index = -1;
};

const handleArrowRight = (event: KeyboardEvent) => {
if (state.index === -1) return;
event.preventDefault();

handleSelectHint(state.index);
};

const keyMap: Record<string, (event: KeyboardEvent) => void> = {
ArrowUp: handleArrowUp,
ArrowDown: handleArrowDown,
ArrowLeft: handleArrowLeft,
ArrowRight: handleArrowRight,
};

const handleKeydown = (event: KeyboardEvent) => {
const { key } = event;
if (!hasArrowKey(key)) return;

keyMap[key]?.(event);
};

let currentFocus = false;
let timerId = -1;

const handleFocus = () => {
currentFocus = true;
};

const handleFocusout = (event: FocusEvent) => {
currentFocus = false;

window.clearTimeout(timerId);
timerId = window.setTimeout(() => {
const input = root.value?.querySelector('input');
const isFocus = currentFocus && input && props.edit;

if (isFocus) {
lastCursorFocus(input);
} else {
props.onBlur?.(event);
}
}, 1);
};

const handleInput = (event: InputEvent) => {
state.holdFilter = false;
props.onInput?.(event);
};

onMounted(() => {
const { store } = app.value;
const { settings } = store.state;

addUnsubscribe(
watch(props).subscribe(propName => {
if (propName !== 'value') return;
setHints();
}),
watch(settings).subscribe(propName => {
if (propName !== 'database') return;
state.holdFilter = false;
setHints();
})
);
});

return () => html`
<div
class=${styles.root}
${ref(root)}
tabindex="-1"
@focus=${handleFocus}
@focusin=${handleFocus}
@focusout=${handleFocusout}
>
<${EditInput}
placeholder="dataType"
width=${props.width}
value=${props.value}
focus=${props.focus}
edit=${props.edit}
autofocus=${true}
.onInput=${handleInput}
.onKeydown=${handleKeydown}
/>
${props.edit
? html`
<div class=${styles.hint}>
${repeat(
state.hints,
hint => hint.name,
(hint, index) =>
html`
<div
class=${{ selected: index === state.index }}
@click=${() => handleSelectHint(index)}
>
<${HighlightedText}
searchWords=${[props.value]}
textToHighlight=${hint.name}
/>
</div>
`
)}
</div>
`
: null}
</div>
`;
};

export default ColumnDataType;
Loading

0 comments on commit 71d0bdb

Please sign in to comment.