From 875d0256f23565c3f81714d29fff25ba23a7d950 Mon Sep 17 00:00:00 2001 From: Ruslan Palkin <53060411+rusandorx@users.noreply.github.com> Date: Fri, 27 Oct 2023 12:29:19 +0300 Subject: [PATCH] feat: add options for placeholders (#141) --- .../base/BaseSchema/BaseSchemaSpecs/index.ts | 7 +++---- src/extensions/behavior/Placeholder/index.ts | 14 ++++++++++++-- src/extensions/behavior/index.ts | 4 +++- .../markdown/Deflist/DeflistSpecs/index.ts | 8 +++++++- .../markdown/Deflist/DeflistSpecs/spec.ts | 16 +++++++++++++--- .../markdown/Heading/HeadingSpecs/index.ts | 10 +++++++--- .../yfm/Checkbox/CheckboxSpecs/index.ts | 5 ++++- .../yfm/Checkbox/CheckboxSpecs/spec.ts | 7 ++++++- src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts | 7 ++++++- src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts | 8 +++++++- src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts | 16 +++++++++++++--- .../yfm/YfmHeading/YfmHeadingSpecs/index.ts | 10 +++++++--- src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts | 5 ++++- src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts | 11 +++++++++-- .../yfm/YfmTable/YfmTableSpecs/index.ts | 5 ++++- .../yfm/YfmTable/YfmTableSpecs/spec.ts | 11 +++++++++-- src/extensions/yfm/YfmTabs/YfmTabsSpecs/index.ts | 3 +++ src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts | 11 +++++++++-- src/utils/placeholder.ts | 5 +++-- 19 files changed, 129 insertions(+), 34 deletions(-) diff --git a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts index f452d0f5..55d77f84 100644 --- a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts +++ b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts @@ -11,12 +11,11 @@ export enum BaseNode { export const pType = nodeTypeFactory(BaseNode.Paragraph); export type BaseSchemaSpecsOptions = { + // This cannot be passed through placeholder option of BehaviorPreset because BasePreset initializes first paragraphPlaceholder?: NonNullable['content']; }; export const BaseSchemaSpecs: ExtensionAuto = (builder, opts) => { - const {paragraphPlaceholder} = opts; - builder .addNode(BaseNode.Doc, () => ({ spec: { @@ -45,9 +44,9 @@ export const BaseSchemaSpecs: ExtensionAuto = (builder, toDOM() { return ['p', 0]; }, - placeholder: paragraphPlaceholder + placeholder: opts.paragraphPlaceholder ? { - content: paragraphPlaceholder, + content: opts.paragraphPlaceholder, alwaysVisible: false, } : undefined, diff --git a/src/extensions/behavior/Placeholder/index.ts b/src/extensions/behavior/Placeholder/index.ts index 22614c31..f8263003 100644 --- a/src/extensions/behavior/Placeholder/index.ts +++ b/src/extensions/behavior/Placeholder/index.ts @@ -7,7 +7,7 @@ import {cn} from '../../../classname'; import type {ExtensionAuto} from '../../../core'; import {isNodeEmpty} from '../../../utils/nodes'; import {isTextSelection} from '../../../utils/selection'; -import {getPlaceholderContent} from '../../../utils/placeholder'; +import {getPlaceholderContent, PlaceholderOptions} from '../../../utils/placeholder'; import './index.scss'; @@ -97,7 +97,8 @@ type WidgetsMap = Record; const pluginKey = new PluginKey('placeholder_plugin'); -export const Placeholder: ExtensionAuto = (builder) => { +export const Placeholder: ExtensionAuto = (builder, opts) => { + builder.context.set('placeholder', opts); builder.addPlugin( () => new Plugin({ @@ -120,6 +121,7 @@ export const Placeholder: ExtensionAuto = (builder) => { apply: applyState, }, }), + builder.Priority.VeryHigh, ); }; @@ -231,3 +233,11 @@ declare module 'prosemirror-model' { }; } } + +declare global { + namespace YfmEditor { + interface Context { + placeholder: PlaceholderOptions; + } + } +} diff --git a/src/extensions/behavior/index.ts b/src/extensions/behavior/index.ts index 6f05159d..299c0e4b 100644 --- a/src/extensions/behavior/index.ts +++ b/src/extensions/behavior/index.ts @@ -6,6 +6,7 @@ import {Cursor, CursorOptions} from './Cursor'; import {History, HistoryOptions} from './History'; import {Clipboard, ClipboardOptions} from './Clipboard'; import {ReactRendererExtension, ReactRenderer} from './ReactRenderer'; +import {PlaceholderOptions} from '../../utils/placeholder'; export * from './Cursor'; export * from './History'; @@ -18,13 +19,14 @@ export type BehaviorPresetOptions = { cursor?: CursorOptions; history?: HistoryOptions; clipboard?: ClipboardOptions; + placeholder?: PlaceholderOptions; reactRenderer: ReactRenderer; }; export const BehaviorPreset: ExtensionAuto = (builder, opts) => { builder .use(Selection) - .use(Placeholder) + .use(Placeholder, opts.placeholder ?? {}) .use(Cursor, opts.cursor ?? {}) .use(History, opts.history ?? {}) .use(Clipboard, opts.clipboard ?? {}) diff --git a/src/extensions/markdown/Deflist/DeflistSpecs/index.ts b/src/extensions/markdown/Deflist/DeflistSpecs/index.ts index a64c5ab6..4e862927 100644 --- a/src/extensions/markdown/Deflist/DeflistSpecs/index.ts +++ b/src/extensions/markdown/Deflist/DeflistSpecs/index.ts @@ -13,12 +13,18 @@ export const defTermType = nodeTypeFactory(DeflistNode.Term); export const defDescType = nodeTypeFactory(DeflistNode.Desc); export type DeflistSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ deflistTermPlaceholder?: NonNullable['content']; + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ deflistDescPlaceholder?: NonNullable['content']; }; export const DeflistSpecs: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); + const spec = getSpec(opts, builder.context.get('placeholder')); builder.configureMd((md) => md.use(deflistPlugin)); builder diff --git a/src/extensions/markdown/Deflist/DeflistSpecs/spec.ts b/src/extensions/markdown/Deflist/DeflistSpecs/spec.ts index 51a5e5f7..a52d96dc 100644 --- a/src/extensions/markdown/Deflist/DeflistSpecs/spec.ts +++ b/src/extensions/markdown/Deflist/DeflistSpecs/spec.ts @@ -1,13 +1,17 @@ import type {NodeSpec} from 'prosemirror-model'; import type {DeflistSpecsOptions} from './index'; import {DeflistNode} from './const'; +import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_PLACEHOLDERS = { Term: 'Definition term', Desc: 'Definition description', }; -export const getSpec = (opts?: DeflistSpecsOptions): Record => ({ +export const getSpec = ( + opts?: DeflistSpecsOptions, + placeholder?: PlaceholderOptions, +): Record => ({ [DeflistNode.List]: { group: 'block', content: `(${DeflistNode.Term} ${DeflistNode.Desc})+`, @@ -29,7 +33,10 @@ export const getSpec = (opts?: DeflistSpecsOptions): Record 'Heading ' + node.attrs[headingLevelAttr]; export type HeadingSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ headingPlaceholder?: NonNullable['content']; }; export const HeadingSpecs: ExtensionAuto = (builder, opts) => { - const {headingPlaceholder} = opts ?? {}; - builder.addNode(headingNodeName, () => ({ spec: { attrs: {[headingLevelAttr]: {default: 1}}, @@ -33,7 +34,10 @@ export const HeadingSpecs: ExtensionAuto = (builder, opts) return ['h' + node.attrs[headingLevelAttr], 0]; }, placeholder: { - content: headingPlaceholder ?? DEFAULT_PLACEHOLDER, + content: + builder.context.get('placeholder')?.heading ?? + opts.headingPlaceholder ?? + DEFAULT_PLACEHOLDER, alwaysVisible: true, }, }, diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts index 99cf0bc0..9e363beb 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts @@ -14,6 +14,9 @@ export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label); export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input); export type CheckboxSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ checkboxLabelPlaceholder?: NonNullable['content']; inputView?: YENodeSpec['view']; labelView?: YENodeSpec['view']; @@ -21,7 +24,7 @@ export type CheckboxSpecsOptions = { }; export const CheckboxSpecs: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); + const spec = getSpec(opts, builder.context.get('placeholder')); builder .configureMd((md) => checkboxPlugin(md, {idPrefix, divClass: b()})) diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts index de362778..537e1aba 100644 --- a/src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts @@ -1,11 +1,13 @@ import type {NodeSpec} from 'prosemirror-model'; import type {CheckboxSpecsOptions} from './index'; import {b, CheckboxNode} from '../const'; +import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_LABEL_PLACEHOLDER = 'Checkbox'; export const getSpec = ( opts?: Pick, + placeholder?: PlaceholderOptions, ): Record => ({ [CheckboxNode.Checkbox]: { group: 'block', @@ -54,7 +56,10 @@ export const getSpec = ( }, escapeText: false, placeholder: { - content: opts?.checkboxLabelPlaceholder ?? DEFAULT_LABEL_PLACEHOLDER, + content: + placeholder?.[CheckboxNode.Label] ?? + opts?.checkboxLabelPlaceholder ?? + DEFAULT_LABEL_PLACEHOLDER, alwaysVisible: true, }, toDOM(node) { diff --git a/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts b/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts index c962e080..0002f661 100644 --- a/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts +++ b/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts @@ -18,10 +18,15 @@ type ImsizeTypedAttributes = { export {ImgSizeAttr}; export type ImgSizeSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ placeholder?: NodeSpec['placeholder']; }; export const ImgSizeSpecs: ExtensionAuto = (builder, opts) => { + const placeholderContent = builder.context.get('placeholder')?.imgSize; + builder.configureMd((md) => md.use(imsize, {log})); builder.addNode(imageNodeName, () => ({ spec: { @@ -33,7 +38,7 @@ export const ImgSizeSpecs: ExtensionAuto = (builder, opts) [ImgSizeAttr.Height]: {default: null}, [ImgSizeAttr.Width]: {default: null}, }, - placeholder: opts.placeholder, + placeholder: placeholderContent ? {content: placeholderContent} : opts.placeholder, group: 'inline', draggable: true, parseDOM: [ diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts index d861c577..60bab752 100644 --- a/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts @@ -18,12 +18,18 @@ export type YfmCutSpecsOptions = { cutView?: YENodeSpec['view']; cutTitleView?: YENodeSpec['view']; cutContentView?: YENodeSpec['view']; + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ yfmCutTitlePlaceholder?: NonNullable['content']; + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ yfmCutContentPlaceholder?: NonNullable['content']; }; export const YfmCutSpecs: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); + const spec = getSpec(opts, builder.context.get('placeholder')); builder .configureMd((md) => md.use(yfmPlugin, {log})) diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts index c9b30c94..f601754d 100644 --- a/src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts @@ -1,13 +1,17 @@ import type {NodeSpec} from 'prosemirror-model'; import type {YfmCutSpecsOptions} from './index'; import {CutNode} from '../const'; +import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_PLACEHOLDERS = { Title: 'Cut title', Content: 'Cut content', }; -export const getSpec = (opts?: YfmCutSpecsOptions): Record => ({ +export const getSpec = ( + opts?: YfmCutSpecsOptions, + placeholder?: PlaceholderOptions, +): Record => ({ [CutNode.Cut]: { attrs: {class: {default: 'yfm-cut'}}, content: `${CutNode.CutTitle} ${CutNode.CutContent}`, @@ -31,7 +35,10 @@ export const getSpec = (opts?: YfmCutSpecsOptions): Record => return ['div', node.attrs, 0]; }, placeholder: { - content: opts?.yfmCutTitlePlaceholder ?? DEFAULT_PLACEHOLDERS.Title, + content: + placeholder?.[CutNode.CutTitle] ?? + opts?.yfmCutTitlePlaceholder ?? + DEFAULT_PLACEHOLDERS.Title, alwaysVisible: true, }, selectable: false, @@ -48,7 +55,10 @@ export const getSpec = (opts?: YfmCutSpecsOptions): Record => return ['div', node.attrs, 0]; }, placeholder: { - content: opts?.yfmCutContentPlaceholder ?? DEFAULT_PLACEHOLDERS.Content, + content: + placeholder?.[CutNode.CutContent] ?? + opts?.yfmCutContentPlaceholder ?? + DEFAULT_PLACEHOLDERS.Content, alwaysVisible: true, }, selectable: false, diff --git a/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts index 16afb007..01142f22 100644 --- a/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts +++ b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts @@ -8,13 +8,14 @@ const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[YfmHeadingAt export {YfmHeadingAttr} from './const'; export type YfmHeadingSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ headingPlaceholder?: NonNullable['content']; }; /** YfmHeading extension needs markdown-it-attrs plugin */ export const YfmHeadingSpecs: ExtensionAuto = (builder, opts) => { - const {headingPlaceholder} = opts ?? {}; - builder.addNode(headingNodeName, () => ({ spec: { attrs: { @@ -52,7 +53,10 @@ export const YfmHeadingSpecs: ExtensionAuto = (builder, ]; }, placeholder: { - content: headingPlaceholder ?? DEFAULT_PLACEHOLDER, + content: + builder.context.get('placeholder')?.heading ?? + opts.headingPlaceholder ?? + DEFAULT_PLACEHOLDER, alwaysVisible: true, }, }, diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts index 08ddcc20..5711eb99 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts @@ -12,11 +12,14 @@ export {NoteNode as YfmNoteNode} from './const'; export {noteType, noteTitleType} from './utils'; export type YfmNoteSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ yfmNoteTitlePlaceholder?: NonNullable['content']; }; export const YfmNoteSpecs: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); + const spec = getSpec(opts, builder.context.get('placeholder')); builder .configureMd((md) => md.use(yfmPlugin, {log})) diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts index 3d12128b..e4b9ae3b 100644 --- a/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts @@ -1,10 +1,14 @@ import type {NodeSpec} from 'prosemirror-model'; import {YfmNoteSpecsOptions} from './index'; import {NoteAttrs, NoteNode} from './const'; +import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_TITLE_PLACEHOLDER = 'Note'; -export const getSpec = (opts?: YfmNoteSpecsOptions): Record => ({ +export const getSpec = ( + opts?: YfmNoteSpecsOptions, + placeholder?: PlaceholderOptions, +): Record => ({ [NoteNode.Note]: { attrs: { [NoteAttrs.Class]: {default: 'yfm-note yfm-accent-info'}, @@ -45,7 +49,10 @@ export const getSpec = (opts?: YfmNoteSpecsOptions): Record selectable: false, allowSelection: false, placeholder: { - content: opts?.yfmNoteTitlePlaceholder ?? DEFAULT_TITLE_PLACEHOLDER, + content: + placeholder?.[NoteNode.NoteTitle] ?? + opts?.yfmNoteTitlePlaceholder ?? + DEFAULT_TITLE_PLACEHOLDER, alwaysVisible: true, }, complex: 'leaf', diff --git a/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts index 6aea3626..879bbcef 100644 --- a/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts @@ -12,11 +12,14 @@ export {YfmTableNode} from './const'; export {yfmTableType, yfmTableBodyType, yfmTableRowType, yfmTableCellType} from './utils'; export type YfmTableSpecsOptions = { + /** + * @deprecated: use placeholder option in BehaviorPreset instead. + */ yfmTableCellPlaceholder?: NonNullable['content']; }; export const YfmTableSpecs: ExtensionWithOptions = (builder, options) => { - const spec = getSpec(options); + const spec = getSpec(options, builder.context.get('placeholder')); builder .configureMd((md) => md.use(yfmTable, {log})) diff --git a/src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts index 1cae6846..66f33707 100644 --- a/src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts @@ -2,10 +2,14 @@ import type {NodeSpec} from 'prosemirror-model'; import {TableRole} from '../../../../table-utils'; import type {YfmTableSpecsOptions} from './index'; import {YfmTableNode} from './const'; +import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_CELL_PLACEHOLDER = 'Table cell'; -export const getSpec = (opts?: YfmTableSpecsOptions): Record => ({ +export const getSpec = ( + opts?: YfmTableSpecsOptions, + placeholder?: PlaceholderOptions, +): Record => ({ [YfmTableNode.Table]: { group: 'block yfm-table', content: `${YfmTableNode.Body}`, @@ -67,7 +71,10 @@ export const getSpec = (opts?: YfmTableSpecsOptions): Record['content']; tabView?: YENodeSpec['view']; tabsListView?: YENodeSpec['view']; diff --git a/src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts index 071b38a3..80684ebc 100644 --- a/src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts @@ -1,12 +1,16 @@ import type {NodeSpec} from 'prosemirror-model'; import {YfmTabsSpecsOptions} from '.'; import {TabAttrs, TabPanelAttrs, TabsAttrs, TabsListAttrs, TabsNode} from './const'; +import {PlaceholderOptions} from '../../../../utils/placeholder'; const DEFAULT_PLACEHOLDERS = { TabTitle: 'Tab title', }; -export const getSpec: (opts: YfmTabsSpecsOptions) => Record = (opts) => ({ +export const getSpec: ( + opts: YfmTabsSpecsOptions, + placeholder?: PlaceholderOptions, +) => Record = (opts, placeholder) => ({ [TabsNode.Tab]: { attrs: { [TabAttrs.id]: {default: 'unknown'}, @@ -27,7 +31,10 @@ export const getSpec: (opts: YfmTabsSpecsOptions) => Record return ['div', node.attrs, 0]; }, placeholder: { - content: opts?.tabPlaceholder ?? DEFAULT_PLACEHOLDERS.TabTitle, + content: + placeholder?.[TabsNode.Tab] ?? + opts?.tabPlaceholder ?? + DEFAULT_PLACEHOLDERS.TabTitle, alwaysVisible: true, }, selectable: false, diff --git a/src/utils/placeholder.ts b/src/utils/placeholder.ts index c5bdee02..24d5943b 100644 --- a/src/utils/placeholder.ts +++ b/src/utils/placeholder.ts @@ -1,5 +1,6 @@ -import type {Node} from 'prosemirror-model'; -import type {} from '../extensions/behavior/Placeholder'; +import type {Node, NodeSpec} from 'prosemirror-model'; + +export type PlaceholderOptions = Record['content']>; export const getPlaceholderContent = (node: Node, parent?: Node | null) => { const content = node.type.spec.placeholder?.content || '';