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

feat(OrderedList UI): support start property #90

Merged
merged 12 commits into from
Oct 23, 2024
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@
{
type: 'List',
data: {
style: 'ordered',
start: 2,
items: [
{
content: "Canon",
Expand Down
3 changes: 2 additions & 1 deletion src/ListRenderer/ChecklistRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { IconCheck } from '@codexteam/icons';
import type { ChecklistItemMeta } from '../types/ItemMeta';
import type { NestedListConfig } from '../types/ListParams';
import * as Dom from '@editorjs/dom';
import { DefaultListCssClasses, CssPrefix } from './ListRenderer';
import { DefaultListCssClasses } from './ListRenderer';
import type { ListCssClasses, ListRendererInterface } from './ListRenderer';
import { CssPrefix } from '../styles/CssPrefix';

/**
* Interface that represents all list used only in unordered list rendering
Expand Down
6 changes: 1 addition & 5 deletions src/ListRenderer/ListRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/**
* Default css prefix for list
*/
export const CssPrefix = 'cdx-list';

import { CssPrefix } from '../styles/CssPrefix';
/**
* CSS classes for the List Tool
*/
Expand Down
3 changes: 2 additions & 1 deletion src/ListRenderer/OrderedListRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { OrderedListItemMeta } from '../types/ItemMeta';
import type { NestedListConfig } from '../types/ListParams';
import * as Dom from '@editorjs/dom';
import { DefaultListCssClasses, CssPrefix } from './ListRenderer';
import { DefaultListCssClasses } from './ListRenderer';
import type { ListCssClasses, ListRendererInterface } from './ListRenderer';
import { CssPrefix } from '../styles/CssPrefix';

/**
* Interface that represents all list used only in unordered list rendering
Expand Down
3 changes: 2 additions & 1 deletion src/ListRenderer/UnorderedListRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { UnorderedListItemMeta } from '../types/ItemMeta';
import type { NestedListConfig } from '../types/ListParams';
import * as Dom from '@editorjs/dom';
import { DefaultListCssClasses, CssPrefix } from './ListRenderer';
import { DefaultListCssClasses } from './ListRenderer';
import type { ListCssClasses, ListRendererInterface } from './ListRenderer';
import { CssPrefix } from '../styles/CssPrefix';

/**
* Interface that represents all list used only in unordered list rendering
Expand Down
4 changes: 2 additions & 2 deletions src/ListRenderer/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CheckListRenderer } from './ChecklistRenderer';
import { OrderedListRenderer } from './OrderedListRenderer';
import { UnorderedListRenderer } from './UnorderedListRenderer';
import { DefaultListCssClasses, CssPrefix } from './ListRenderer';
import { DefaultListCssClasses } from './ListRenderer';

export { CheckListRenderer, OrderedListRenderer, UnorderedListRenderer, DefaultListCssClasses, CssPrefix };
export { CheckListRenderer, OrderedListRenderer, UnorderedListRenderer, DefaultListCssClasses };
34 changes: 32 additions & 2 deletions src/ListTabulator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,13 @@ export default class ListTabulator<Renderer extends ListRenderer> {
);
}

/**
* Set start property value from initial data
*/
if (this.data.start !== undefined) {
this.changeStartWith(this.data.start);
}

return this.listWrapper;
}

Expand Down Expand Up @@ -188,10 +195,21 @@ export default class ListTabulator<Renderer extends ListRenderer> {
});
};

return {
const composedListItems = listWrapper ? getItems(listWrapper) : [];

let dataToSave: ListData = {
style: this.data.style,
items: listWrapper ? getItems(listWrapper) : [],
items: composedListItems,
};

if (this.data.style === 'ordered') {
dataToSave = {
start: this.data.start,
...dataToSave,
};
}

return dataToSave;
}

/**
Expand Down Expand Up @@ -354,6 +372,16 @@ export default class ListTabulator<Renderer extends ListRenderer> {
return data;
}

/**
* Changes ordered list start property value
* @param index - new value of the start property
*/
public changeStartWith(index: number): void {
this.listWrapper!.style.setProperty('counter-reset', `item ${index - 1}`);

this.data.start = index;
}

/**
* Handles Enter keypress
* @param event - keydown
Expand Down Expand Up @@ -595,6 +623,8 @@ export default class ListTabulator<Renderer extends ListRenderer> {

const newListContent = this.save(newListWrapper);

newListContent.start = this.data.style == 'ordered' ? 1 : undefined;

/**
* Get current list block index
*/
Expand Down
78 changes: 63 additions & 15 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { API, BlockAPI, PasteConfig, ToolboxConfig } from '@editorjs/editorjs';
import type {
BlockToolConstructorOptions,
ToolConfig,
TunesMenuConfig
MenuConfigItem,
ToolConfig
} from '@editorjs/editorjs/types/tools';
import { IconListBulleted, IconListNumbered, IconChecklist } from '@codexteam/icons';
import type { NestedListConfig, ListData, ListDataStyle, ListItem } from './types/ListParams';
Expand All @@ -13,7 +13,9 @@ import type { ListRenderer } from './types/ListRenderer';
/**
* Build styles
*/
import './../styles/index.pcss';
import './styles/list.pcss';
import './styles/input.pcss';
import { renderToolboxInput } from './utils/renderToolboxInput';

/**
* Constructor Params for Nested List Tool, use to pass initial data and settings
Expand Down Expand Up @@ -254,35 +256,81 @@ export default class NestedList {
* Creates Block Tune allowing to change the list style
* @returns array of tune configs
*/
public renderSettings(): TunesMenuConfig {
const tunes = [
public renderSettings(): MenuConfigItem[] {
const defaultTunes: MenuConfigItem[] = [
{
name: 'unordered' as const,
label: this.api.i18n.t('Unordered'),
icon: IconListBulleted,
closeOnActivate: true,
onActivate: () => {
this.listStyle = 'unordered';
},
},
{
name: 'ordered' as const,
label: this.api.i18n.t('Ordered'),
icon: IconListNumbered,
closeOnActivate: true,
onActivate: () => {
this.listStyle = 'ordered';
},
},
{
name: 'checklist' as const,
label: this.api.i18n.t('Checklist'),
icon: IconChecklist,
closeOnActivate: true,
onActivate: () => {
this.listStyle = 'checklist';
},
},
];

return tunes.map(tune => ({
name: tune.name,
icon: tune.icon,
label: tune.label,
isActive: this.data.style === tune.name,
closeOnActivate: true,
onActivate: () => {
this.listStyle = tune.name;
},
}));
if (this.listStyle === 'ordered') {
const startWithElement = renderToolboxInput(
(index: number) => this.changeStartWith(index),
{
value: String(this.data.start ?? 1),
placeholder: '',
attributes: {
type: 'number',
step: '1',
required: 'true',
},
});

const unorderedListTunes: MenuConfigItem[] = [
{
name: 'start with' as const,
label: this.api.i18n.t('Start with'),
children: {
items: [
{
name: 'start with input',
element: startWithElement,
// @ts-expect-error ts(2820) can not use PopoverItem enum from editor.js types
type: 'html',
},
],
},
},
];

defaultTunes.push(...unorderedListTunes);
}

return defaultTunes;
}

/**
* Changes ordered list start property value
* @param index - new value of the start property
*/
private changeStartWith(index: number): void {
this.list?.changeStartWith(index);

this.data.start = index;
neSpecc marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/styles/CssPrefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Default css prefix for list
*/
export const CssPrefix = 'cdx-list';
31 changes: 31 additions & 0 deletions src/styles/input.pcss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.cdx-list-start-with-field {
background: #F8F8F8;
border: 1px solid rgba(226,226,229,0.20);
border-radius: 6px;
padding: 2px;
display: grid;
grid-template-columns: auto auto 1fr;
grid-template-rows: auto;

&__input {
font-size: 14px;
outline: none;
font-weight: 500;
font-family: inherit;
border: 0;
background: transparent;
margin: 0;
padding: 0;
line-height: 22px;
min-width: calc(100% - var(--toolbox-buttons-size) - var(--icon-margin-right));

&--invalid {
background: red;
}

&::placeholder {
color: var(--grayText);
font-weight: 500;
}
}
}
File renamed without changes.
7 changes: 6 additions & 1 deletion src/types/ItemMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ export interface ChecklistItemMeta extends ItemMeta {
/**
* Meta information of ordered list item
*/
export interface OrderedListItemMeta extends ItemMeta {};
export interface OrderedListItemMeta extends ItemMeta {
/**
* If passed, ordered list counters will start with this index
*/
start?: number;
};

/**
* Meta information of unordered list item
Expand Down
4 changes: 4 additions & 0 deletions src/types/ListParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export interface ListData {
* list of first-level elements
*/
items: ListItem[];
/**
* Start property used only in ordered list
*/
start?: number;
}

/**
Expand Down
94 changes: 94 additions & 0 deletions src/utils/renderToolboxInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as Dom from '@editorjs/dom';
import { CssPrefix } from '../styles/CssPrefix';

/**
* Options used in input rendering
*/
interface InputOptions {
/**
* Placeholder, that will be displayed in input
*/
placeholder: string;
/**
* Input will be rendered with this value inside
*/
value?: string;
/**
* Html attributes, that would be added to the input element
*/
attributes?: {
[key: string]: string;
};
}

const css = {
wrapper: `${CssPrefix}-start-with-field`,
input: `${CssPrefix}-start-with-field__input`,
inputInvalid: `${CssPrefix}-start-with-field__input--invalid`,
};

/**
* Method that renders html element for popover start with item
* @param inputCallback - callback method that could change nested list attributes on input
* @param inputOptions - options used in input rendering
* @param inputOptions.value - input will be rendered with this value inside
* @param inputOptions.placeholder - placeholder, that will be displayed in input
* @param inputOptions.attributes - html attributes, that would be added to the input element
* @returns - rendered html element
*/
export function renderToolboxInput(inputCallback: (index: number) => void,
{ value, placeholder, attributes }: InputOptions): HTMLElement {
const startWithElementWrapper = Dom.make('div', css.wrapper);

const input = Dom.make('input', css.input, {
placeholder,
/**
* Used to prevent focusing on the input by Tab key
* (Popover in the Toolbar lays below the blocks,
* so Tab in the last block will focus this hidden input if this property is not set)
*/
tabIndex: -1,
/**
* Value of the start property, if it is not specified, then it is set to one
*/
value,
}) as HTMLInputElement;

/**
* Add passed attributes to the input
*/
for (const attribute in attributes) {
input.setAttribute(attribute, attributes[attribute]);
}

startWithElementWrapper.appendChild(input);

input.addEventListener('input', () => {
const validInput = input.checkValidity();

/**
* If input is invalid and classlist does not contain invalid class, add it
*/
if (!validInput && !input.classList.contains(css.inputInvalid)) {
input.classList.add(css.inputInvalid);
}

/**
* If input is valid and classlist contains invalid class, remove it
*/
if (validInput && input.classList.contains(css.inputInvalid)) {
input.classList.remove(css.inputInvalid);
}

/**
* If input is invalid, than do not change start with attribute
*/
if (!validInput) {
return;
}

inputCallback(Number(input.value));
});

return startWithElementWrapper;
}
Loading
Loading