diff --git a/lab/dms/client/component/x-file-item-upload.ts b/lab/dms/client/component/x-file-item-upload.ts index 3ec73dcc7573ccee8c755235f809c1f60f66c975..b79d63d14b6eec546d6da14b0ba316b07e49b0a2 100644 --- a/lab/dms/client/component/x-file-item-upload.ts +++ b/lab/dms/client/component/x-file-item-upload.ts @@ -46,8 +46,9 @@ export class XFileItemUpload extends ChemistryLitElement { const resolvedFile = (await this._uploadClass?.getByID(file)( this._uploadClass.allFields, )) as Maybe<AdornisFile>; - if (resolvedFile && resolvedFile.meta && this.file?.name.split('.')[0]) { - const name = this.file.name.split('.')[0]!; + + const name = this.file?.name.substring(0, this.file?.name.lastIndexOf('.')); + if (resolvedFile && resolvedFile.meta && name) { resolvedFile.meta.fileName = name; await resolvedFile.save(); } diff --git a/lab/dms/client/component/x-file-list.ts b/lab/dms/client/component/x-file-list.ts index 97abc5bef4c935899d9f7a3eca1c361af0d992cf..37374b8c5a57e775484d29e386af399fdcfde9d0 100644 --- a/lab/dms/client/component/x-file-list.ts +++ b/lab/dms/client/component/x-file-list.ts @@ -147,7 +147,7 @@ export class XFileList extends ChemistryLitElement { fontSize: '16px', })} > - ${file.meta?.fileName}.${file.meta?.extension} + ${file.meta?.fileName} </x-text> <x-text ${css({ fontSize: '12px' })}> diff --git a/lab/html-based-buildify/client/BuildifyLitElement.ts b/lab/html-based-buildify/client/BuildifyLitElement.ts index a7e27293f267250494d5c90d718db36ade77e053..78a5ef475d046445c31190f2473437a09fc97689 100644 --- a/lab/html-based-buildify/client/BuildifyLitElement.ts +++ b/lab/html-based-buildify/client/BuildifyLitElement.ts @@ -48,7 +48,7 @@ export class BuildifyLitElement<T extends HTMLBase> extends ChemistryLitElement @property({ attribute: 'data', type: String }) data = new RXController(this, undefined); @property({ attribute: 'no-content', type: Boolean }) hasNoContent = false; - @state() protected readonly _constructedData = new RXController( + @state() readonly _constructedData = new RXController( this, this.data.observable.pipe( filter(Boolean), diff --git a/lab/html-based-buildify/client/marks/font-size.ts b/lab/html-based-buildify/client/marks/font-size.ts index b5bd1ad42aa8f23673c893f6844af34bbe45d07a..5f284c73b6467bc32d03b02529da1f656cb75a94 100644 --- a/lab/html-based-buildify/client/marks/font-size.ts +++ b/lab/html-based-buildify/client/marks/font-size.ts @@ -22,7 +22,7 @@ export const fontSize: IMarkConfig = { export const FONT_SIZE_MENU_ITEM = (fontSize: string) => createFontSizeMenuItem(fontSize); -function createFontSizeMenuItem(fontSize: string): IMenuItem { +export function createFontSizeMenuItem(fontSize: string): IMenuItem { return { group: MenuItemGroup.TEXT_STYLE, label: fontSize, @@ -37,27 +37,3 @@ function createFontSizeMenuItem(fontSize: string): IMenuItem { }, }; } - -// export function fontSizeMenu() { -// const fontSizes = [ -// '8px', -// '10px', -// '12px', -// '14px', -// '16px', -// '18px', -// '20px', -// '24px', -// '30px', -// '36px', -// '48px', -// '60px', -// '72px', -// ]; - -// const items = fontSizes.map(size => createFontSizeMenuItem(size)); - -// return new Dropdown(items, { -// label: '', -// }); -// } diff --git a/lab/html-based-buildify/client/menu-items/CollapsibleMenuItem.ts b/lab/html-based-buildify/client/menu-items/CollapsibleMenuItem.ts new file mode 100644 index 0000000000000000000000000000000000000000..453b17b88835ed4a2bca7f16610cce1e67b35640 --- /dev/null +++ b/lab/html-based-buildify/client/menu-items/CollapsibleMenuItem.ts @@ -0,0 +1,104 @@ +import type { Styles } from '@adornis/ass/style.js'; +import { ChemistryLitElement } from '@adornis/chemistry/chemistry-lit-element.js'; +import { css } from '@adornis/chemistry/directives/css.js'; +import { html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import type { EditorView } from 'prosemirror-view'; +import type { IMenuItem } from '../types.js'; +import { renderMenuItem, renderMenuItemIcon } from '../x-prosemirror-toolbar.js'; + +// Note: +// The collapsible uses its own class to represent a state +// Do not recreate this MenuItem on render cycles! State will be lost. (isOpen state) + +export class CollapsibleMenuItem implements IMenuItem { + isOpen: boolean = false; + menuItems: IMenuItem[]; + label: string; + icon: string | undefined; + group?: string | undefined; + + isActive?: ((editorView: EditorView) => boolean) | undefined; + shouldVisualize?: ((editorView: EditorView) => boolean) | undefined; + + private _menu: CollapsibleMenu; + constructor({ + label, + group, + menuItems, + icon, + }: { + label: string; + group?: string; + menuItems: IMenuItem[]; + icon: string; + }) { + this.label = label; + this.group = group; + this.menuItems = menuItems; + this.icon = icon; + + this._menu = document.createElement('collapsible-menu') as CollapsibleMenu; + this._menu.addEventListener('mousedown', e => { + e.preventDefault(); + }); + this._menu.label = label; + this._menu.icon = icon; + this._menu.menuItems = menuItems; + this._menu.isOpen = this.isOpen; + } + + render(view: EditorView) { + this._menu.view = view; + this._menu.isOpen = this.isOpen; + return html` ${this._menu} `; + } + + run(editorView: EditorView) { + this.isOpen = !this._menu.isOpen; + this._menu.isOpen = this.isOpen; + } +} + +@customElement('collapsible-menu') +export class CollapsibleMenu extends ChemistryLitElement { + label!: string; + icon!: string; + menuItems!: IMenuItem[]; + view!: EditorView; + @state() isOpen: boolean = false; + + override render() { + return html` + <x-flex horizontal crossaxis-center space="sm" wrap ${css({ borderBottom: this.isOpen ? '2px solid #333' : '' })}> + ${renderMenuItemIcon({ view: this.view, isActive: this.isOpen, icon: this.icon, run: () => {} })} + ${this.isOpen + ? html` + <x-flex + horizontal + crossaxis-center + space="sm" + wrap + @click=${e => { + e.preventDefault(); + e.stopPropagation(); + this.requestUpdate(); + }} + > + ${this.menuItems.map(item => renderMenuItem(item, this.view))} + </x-flex> + ` + : nothing} + </x-flex> + `; + } + + override styles() { + return [ + ...super.styles(), + { + ':host': {}, + }, + ] as Styles[]; + } +} diff --git a/lab/html-based-buildify/client/menu-items/CopySelectionToClipboard.ts b/lab/html-based-buildify/client/menu-items/CopySelectionToClipboard.ts index 2571b976b48dfd4a13f1af43cf1cab46ad23c039..c9e9bfea65a907b6d47c6cfa9cea98f214d73ee4 100644 --- a/lab/html-based-buildify/client/menu-items/CopySelectionToClipboard.ts +++ b/lab/html-based-buildify/client/menu-items/CopySelectionToClipboard.ts @@ -1,5 +1,7 @@ import { XDialog } from '@adornis/dialog/x-dialog.js'; import { baseKeymap } from 'prosemirror-commands'; +import { DOMParser } from 'prosemirror-model'; +import {} from 'prosemirror-state'; import type { IMenuItem } from '../types.js'; export const CopySelectionMenuItem: IMenuItem = { @@ -27,49 +29,55 @@ export const CopySelectionMenuItem: IMenuItem = { }, }; -// export const PasteFromClipboardMenuItem: IMenuItem = { -// label: 'Einfügen', -// icon: 'paste', -// run: async view => { -// try { -// // Lese den HTML-Inhalt aus der Zwischenablage -// const clipboardItems = await navigator.clipboard.read(); -// for (const item of clipboardItems) { -// if (item.types.includes('text/html')) { -// const htmlBlob = await item.getType('text/html'); -// const htmlText = await htmlBlob.text(); -// const { state, dispatch } = view; -// const parser = DOMParser.fromSchema(state.schema); -// const doc = parser.parseSlice(htmlText, 'text/html'); -// const body = doc.body; +export const PasteFromClipboardMenuItem: IMenuItem = { + label: 'Einfügen', + icon: 'arrow_upward', + run: async view => { + const { state, dispatch } = view; + const { tr, schema } = state; + + try { + // Überprüfen, ob die Zwischenablage-API verfügbar ist + if (!navigator.clipboard || !navigator.clipboard.read) { + console.error('Die Zwischenablage-API wird in diesem Browser nicht unterstützt.'); + return; + } -// // Verwende den ProseMirror-Parser, um den HTML-Inhalt in Nodes umzuwandeln -// const slice = Slice.fromJSON(state.schema, doc.toJSON()); + // Lesen der Daten aus der Zwischenablage + const clipboardItems = await navigator.clipboard.read(); + let htmlContent: string | null = null; + + // Durchsuchen der ClipboardItems nach HTML-Inhalten + for (const item of clipboardItems) { + if (item.types.includes('text/html')) { + const blob = await item.getType('text/html'); + htmlContent = await blob.text(); + break; + } + } + + // Wenn kein HTML-Inhalt gefunden wurde, eine Warnung ausgeben + if (!htmlContent) { + console.warn('Die Zwischenablage enthält keinen HTML-Inhalt.'); + return; + } -// if (slice) { -// const { tr } = state; -// tr.replaceSelection(slice); -// dispatch(tr); -// await XDialog.alert('Der Inhalt aus der Zwischenablage wurde eingefügt.'); -// } else { -// await XDialog.alert('Fehler beim Einfügen des Inhalts: Kein gültiger Slice.'); -// } -// } else if (item.types.includes('text/plain')) { -// const textBlob = await item.getType('text/plain'); -// const text = await textBlob.text(); -// const { state, dispatch } = view; -// const { tr } = state; -// tr.insertText(text, state.selection.from, state.selection.to); -// dispatch(tr); -// await XDialog.alert('Der Inhalt aus der Zwischenablage wurde eingefügt.'); -// } -// } -// } catch (err) { -// await XDialog.alert('Fehler beim Einfügen aus der Zwischenablage: ' + err.message); -// } -// }, -// shouldVisualize: view => { -// // Die Visualisierung basiert darauf, ob die Zwischenablage-Daten verfügbar sind -// return navigator.clipboard && navigator.clipboard.read !== undefined; -// }, -// }; + // Erstellen eines temporären DOM-Elements zum Parsen des HTML-Inhalts + const tempElement = document.createElement('div'); + tempElement.innerHTML = htmlContent; + + // Konvertieren des HTML-Inhalts in ein ProseMirror-Dokument + const fragment = DOMParser.fromSchema(schema).parse(tempElement); + + // Einfügen des Inhalts in das Dokument + tr.replaceSelectionWith(fragment); + dispatch(tr); + } catch (error) { + console.error('Fehler beim Einfügen aus der Zwischenablage:', error); + } + }, + shouldVisualize: view => { + // Die Visualisierung basiert darauf, ob die Zwischenablage-API verfügbar ist + return navigator.clipboard && typeof navigator.clipboard.read === 'function'; + }, +}; diff --git a/lab/html-based-buildify/client/menu-items/EditGlobalSettings.ts b/lab/html-based-buildify/client/menu-items/EditGlobalSettings.ts index 1e4692f608c2c5862660b110e104066e7a3bd32f..77d55adef15879b4da8d704220c2dc651281a6bb 100644 --- a/lab/html-based-buildify/client/menu-items/EditGlobalSettings.ts +++ b/lab/html-based-buildify/client/menu-items/EditGlobalSettings.ts @@ -3,7 +3,6 @@ import { GlobalSettingsDrawerPanel } from '../global-settings-drawer-panel.js'; import type { IMenuItem } from '../types.js'; export const EditGlobalSettingsMenuItem = (globalSettingsClassName: string): IMenuItem => { - console.log('menu item für global settings: ', globalSettingsClassName); return { label: 'Globale Einstellungen', icon: 'settings', diff --git a/lab/html-based-buildify/client/nodes/grid.ts b/lab/html-based-buildify/client/nodes/grid.ts deleted file mode 100644 index 603a7ffc2d9557e0d654a63fa3466530871f2bf5..0000000000000000000000000000000000000000 --- a/lab/html-based-buildify/client/nodes/grid.ts +++ /dev/null @@ -1,319 +0,0 @@ -/* eslint-disable complexity */ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import type { Styles } from '@adornis/ass/style.js'; -import '@adornis/buildify/client/components/x-buildify-spacing-picker.js'; -import '@adornis/buildify/client/components/x-icon-button.js'; -import { ModeConsumer } from '@adornis/buildify/client/globals/consumer.js'; -import { ChemistryLitElement } from '@adornis/chemistry/chemistry-lit-element.js'; -import { css } from '@adornis/chemistry/directives/css.js'; -import '@adornis/chemistry/elements/components/x-flex'; -import '@adornis/chemistry/elements/components/x-grid.js'; -import '@adornis/chemistry/elements/components/x-icon.js'; -import '@adornis/forms/x-checkbox.js'; -import '@adornis/forms/x-input'; -import { html, nothing } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import type { EditorView } from 'prosemirror-view'; -import { Size } from '../../db/enums.js'; -import { HTMLBaseContainerGridTableView, type HTMLBaseContainerGrid } from '../../db/HTMLBaseContainerGrid.js'; -import { GRID_CELL_KEY, GRID_CELL_SCHEMA, GRID_KEY, GRID_SCHEMA } from '../../schema/nodes/grid.js'; -import { PARAGRAPH_KEY } from '../../schema/nodes/paragraph.js'; -import { BuildifyLitElement } from '../BuildifyLitElement.js'; -import { ContainerEditor } from '../editors/Container.js'; -import { runInsertNode } from '../helper/runInsertNode.js'; -import { MenuItemGroup, type EditorFunc, type INodeConfig } from '../types.js'; -import { isNodeSelection } from '../util.js'; - -export const editor: EditorFunc<HTMLBaseContainerGrid> = ({ - content, - contentController, - controllerBaseKeyPath, - host, -}) => { - const globalColumns = content.value([...controllerBaseKeyPath, 'columns'], undefined) ?? 3; - const desktopColumns = content.value([...controllerBaseKeyPath, 'columns'], Size.DESKTOP); - const tabletColumns = content.value([...controllerBaseKeyPath, 'columns'], Size.TABLET); - const mobileColumns = content.value([...controllerBaseKeyPath, 'columns'], Size.MOBILE); - - const highestColumnsCount = [globalColumns, desktopColumns, tabletColumns, mobileColumns] - .sort((a, b) => b - a) - .at(0) as number; - - if (!content.columnWidths) content.columnWidths = []; - // delete too many entries, if not needed - if (content.columnWidths.length > highestColumnsCount) - content.columnWidths = content.columnWidths.slice(0, highestColumnsCount); - // ensure entries length === columns count - for (let i = 0; i < highestColumnsCount; i++) { - const columnValue = content.columnWidths[i]; - if (columnValue !== undefined) continue; - content.columnWidths[i] = ''; - } - - const countColumns = - content.value([...controllerBaseKeyPath, 'columns'], contentController.size) ?? - content.value([...controllerBaseKeyPath, 'columns'], undefined) ?? - 3; - - return html` - <!-- column definitions --> - <x-flex space="md"> - <x-input - clearable - placeholder="Anzahl Spalten" - type="number" - ${contentController.field(...controllerBaseKeyPath, 'columns')} - @value-picked=${() => { - host.requestUpdate(); - }} - ></x-input> - - <x-infobox> Die Summe der Prozentzahl aller Spaltenbreiten sollte max. 100% betragen </x-infobox> - ${new Array(countColumns).fill(null).map((width, index) => { - return html` - <x-input - clearable - placeholder=${`Breite der ${index + 1}. Spalte`} - ${contentController.field(...controllerBaseKeyPath, 'columnWidths', index)} - ></x-input> - `; - })} - <!-- END: column definitions --> - - <x-infobox> - Um den Abstand der Kacheln innerhalb des Grids zu verändern, kannst du den Wert für den vertikalen oder den - horizontalen Abstand vergrößern und verkleinern. - </x-infobox> - - <x-buildify-spacing-picker - clearable - select - placeholder="Abstand zwischen den Spalten" - ${contentController.field(...controllerBaseKeyPath, 'gridColumnGap')} - select - ></x-buildify-spacing-picker> - <x-buildify-spacing-picker - clearable - select - placeholder="Abstand zwischen den Reihen" - ${contentController.field(...controllerBaseKeyPath, 'gridRowGap')} - select - ></x-buildify-spacing-picker> - - <x-checkbox - .label=${'Anordnung der Items im Grid umdrehen'} - ${contentController.field(...controllerBaseKeyPath, 'revertDirection')} - ></x-checkbox> - - ${content.tableView - ? html` - <x-infobox> Tabellenoptik </x-infobox> - <x-checkbox - .label=${'Horizontale Linien hinzufügen'} - ${contentController.field(...controllerBaseKeyPath, 'tableView', 'hasHorizontalSeperator')} - ></x-checkbox> - <x-checkbox - .label=${'Vertikale Linien hinzufügen'} - ${contentController.field(...controllerBaseKeyPath, 'tableView', 'hasVerticalSeperator')} - ></x-checkbox> - <x-buildify-spacing-picker - placeholder="Breite der Linien" - ${contentController.field(...controllerBaseKeyPath, 'tableView', 'thickness')} - ></x-buildify-spacing-picker> - <x-buildify-color-picker - placeholder="Farbe der Linien" - ${contentController.field(...controllerBaseKeyPath, 'tableView', 'colorSeperators')} - ></x-buildify-color-picker> - <x-icon-button - icon="delete" - text="Tabellenoptik entfernen" - @click=${() => { - content.tableView = null; - host.requestUpdate(); - }} - ></x-icon-button> - ` - : html` - <x-icon-button - icon="add" - text="Tabellenoptik hinzufügen" - @click=${() => { - content.tableView = new HTMLBaseContainerGridTableView({}); - host.requestUpdate(); - }} - ></x-icon-button> - `} - ${ContainerEditor({ - content, - // @ts-expect-error its the same class - contentController, - controllerBaseKeyPath, - host, - })} - </x-flex> - `; -}; - -export const grid: INodeConfig = { - node: { - name: GRID_KEY, - schema: GRID_SCHEMA, - }, - menuItems: [ - { - group: MenuItemGroup.LAYOUT, - label: 'Grid', - icon: 'grid_on', - run: (view: EditorView) => - runInsertNode(GRID_KEY, state => { - return { - content: [ - state.schema.nodes[GRID_CELL_KEY]!.create(null, [ - state.schema.nodes[PARAGRAPH_KEY]!.create(null, [state.schema.text('Hier tippen...')]), - ]), - ], - }; - })(view.state, view), - shouldVisualize: (view: EditorView) => { - return !isNodeSelection(view.state.selection); - }, - }, - ], - editor, -}; - -export const gridCell: INodeConfig = { - node: { - name: GRID_CELL_KEY, - schema: GRID_CELL_SCHEMA, - }, -}; - -@customElement('node-grid') -export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> { - override content({ content, mode }: { content?: HTMLBaseContainerGrid; mode }) { - const columns = content?.columns ? content.columns : 3; - const templateColumns: string[] = []; - for (let i = 0; i < columns; i++) { - const templateValue = content?.getColumnWidthByIndex(i) ?? '1fr'; - templateColumns.push(templateValue); - } - - return html` - <x-grid - columns=${templateColumns.join(' ')} - ${css({ - gridColumnGap: content?.gridColumnGap ?? '', - gridRowGap: content?.gridRowGap ?? '', - padding: content?.padding ?? '', - background: content?.backgroundColor ?? '', - })} - > - <slot></slot> - ${mode === 'edit' - ? html` - <x-flex - ${css({ border: '1px lightgrey dashed', userSelect: 'none', cursor: 'pointer' })} - center - crossaxis-center - @click=${e => { - e.preventDefault(); - e.stopPropagation(); - this.append((document.createElement('div').innerText = 'Hier tippen...')); - }} - > - Zelle hinzufügen - </x-flex> - ` - : nothing} - </x-grid> - `; - } - - override styles() { - return [ - ...super.styles(), - { - ':host': { - borderRadius: this._data.value?.borderRadius ?? '', - }, - 'x-grid>*::slotted(*)': { - boxSizing: 'border-box', - }, - }, - ] as Styles[]; - } -} - -@customElement('node-grid-cell') -export class NodeGridCell extends ChemistryLitElement { - private readonly _consumedMode = ModeConsumer(this); - - @property({ attribute: false }) thickness!: string; - @property({ attribute: false }) color!: string; - @property({ attribute: false }) rowGap!: number; - @property({ attribute: false }) columnGap!: number; - - override render() { - return html` <slot></slot> `; - } - - isEditMode() { - return this._consumedMode.value === 'edit'; - } - - override styles() { - return [ - ...super.styles(), - { - ':host': { - border: this.isEditMode() ? '1px solid grey' : '', - position: 'relative', - }, - ':host(.grid-item)': { - position: 'relative', - overflow: 'visible', - }, - ':host(.grid-item) > *': { - width: '100%', - }, - ':host(.grid-item.child-container-item) > :first-child': { - boxSizing: 'border-box', - height: '100%', - }, - ':host(.grid-line-left)::before': { - content: "''", - position: 'absolute', - left: this.columnGap ? `-${this.columnGap / 2}px` : '0', - top: '0', - bottom: '0', - width: this.thickness, - background: this.color, - zIndex: '10', - }, - ':host(.grid-line-below)::after': { - content: "''", - position: 'absolute', - bottom: this.rowGap ? `-${this.rowGap / 2}px` : '0', - height: this.thickness, - background: this.color, - left: '0', - right: '0', - zIndex: '10', - }, - ':host(.grid-line-left.line-left-max-bottom)::before': { - bottom: this.rowGap ? `-${this.rowGap / 2}px` : '0', - }, - ':host(.grid-line-left.line-left-max-top)::before': { - top: this.rowGap ? `-${this.rowGap / 2}px` : '0', - }, - ':host(.grid-line-below.line-below-max-left)::after': { - left: this.columnGap ? `-${this.columnGap / 2}px` : '0', - }, - ':host(.grid-line-below.line-below-max-right)::after': { - right: this.columnGap ? `-${this.columnGap / 2}px` : '0', - }, - }, - ] as Styles[]; - } -} diff --git a/lab/html-based-buildify/client/nodes/grid/config.ts b/lab/html-based-buildify/client/nodes/grid/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..abd5d945ffe9cd7bbd37d623db0bb85261c49ad0 --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/config.ts @@ -0,0 +1,38 @@ +import type { EditorView } from 'prosemirror-view'; +import { GRID_CELL_KEY, GRID_KEY, GRID_SCHEMA } from '../../../schema/nodes/grid.js'; +import { PARAGRAPH_KEY } from '../../../schema/nodes/paragraph.js'; +import { runInsertNode } from '../../helper/runInsertNode.js'; +import { type INodeConfig, MenuItemGroup } from '../../types.js'; +import { isNodeSelection } from '../../util.js'; +import { editor } from './editor.js'; +import './node-grid.js'; +// import { gridViewPlugin } from './node-view.js'; + +export const grid: INodeConfig = { + node: { + name: GRID_KEY, + schema: GRID_SCHEMA, + }, + menuItems: [ + { + group: MenuItemGroup.LAYOUT, + label: 'Grid', + icon: 'grid_on', + run: (view: EditorView) => + runInsertNode(GRID_KEY, state => { + return { + content: [ + state.schema.nodes[GRID_CELL_KEY]!.create(null, [ + state.schema.nodes[PARAGRAPH_KEY]!.create(null, [state.schema.text('Hier tippen...')]), + ]), + ], + }; + })(view.state, view), + shouldVisualize: (view: EditorView) => { + return !isNodeSelection(view.state.selection); + }, + }, + ], + editor, + // plugins: (schema: Schema) => [gridViewPlugin], +}; diff --git a/lab/html-based-buildify/client/nodes/grid/editor.ts b/lab/html-based-buildify/client/nodes/grid/editor.ts new file mode 100644 index 0000000000000000000000000000000000000000..717e87431f3b5b29ec1547e2cfe4eb7c50dc7d21 --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/editor.ts @@ -0,0 +1,135 @@ +import { Size } from '@adornis/buildify/client/globals/enums.js'; +import { html } from 'lit'; +import { HTMLBaseContainerGrid, HTMLBaseContainerGridTableView } from '../../../db/HTMLBaseContainerGrid.js'; +import { ContainerEditor } from '../../editors/Container.js'; +import type { EditorFunc } from '../../types.js'; + +export const editor: EditorFunc<HTMLBaseContainerGrid> = ({ + content, + contentController, + controllerBaseKeyPath, + host, +}) => { + const globalColumns = content.value([...controllerBaseKeyPath, 'columns'], undefined) ?? 3; + const desktopColumns = content.value([...controllerBaseKeyPath, 'columns'], Size.DESKTOP); + const tabletColumns = content.value([...controllerBaseKeyPath, 'columns'], Size.TABLET); + const mobileColumns = content.value([...controllerBaseKeyPath, 'columns'], Size.MOBILE); + + const highestColumnsCount = [globalColumns, desktopColumns, tabletColumns, mobileColumns] + .sort((a, b) => b - a) + .at(0) as number; + + if (!content.columnWidths) content.columnWidths = []; + // delete too many entries, if not needed + if (content.columnWidths.length > highestColumnsCount) + content.columnWidths = content.columnWidths.slice(0, highestColumnsCount); + // ensure entries length === columns count + for (let i = 0; i < highestColumnsCount; i++) { + const columnValue = content.columnWidths[i]; + if (columnValue !== undefined) continue; + content.columnWidths[i] = ''; + } + + const countColumns = + content.value([...controllerBaseKeyPath, 'columns'], contentController.size) ?? + content.value([...controllerBaseKeyPath, 'columns'], undefined) ?? + 3; + + return html` + <!-- column definitions --> + <x-flex space="md"> + <x-input + clearable + placeholder="Anzahl Spalten" + type="number" + ${contentController.field(...controllerBaseKeyPath, 'columns')} + @value-picked=${() => { + host.requestUpdate(); + }} + ></x-input> + + <x-infobox> Die Summe der Prozentzahl aller Spaltenbreiten sollte max. 100% betragen </x-infobox> + ${new Array(countColumns).fill(null).map((width, index) => { + return html` + <x-input + clearable + placeholder=${`Breite der ${index + 1}. Spalte`} + ${contentController.field(...controllerBaseKeyPath, 'columnWidths', index)} + ></x-input> + `; + })} + <!-- END: column definitions --> + + <x-infobox> + Um den Abstand der Kacheln innerhalb des Grids zu verändern, kannst du den Wert für den vertikalen oder den + horizontalen Abstand vergrößern und verkleinern. + </x-infobox> + + <x-buildify-spacing-picker + clearable + select + placeholder="Abstand zwischen den Spalten" + ${contentController.field(...controllerBaseKeyPath, 'gridColumnGap')} + select + ></x-buildify-spacing-picker> + <x-buildify-spacing-picker + clearable + select + placeholder="Abstand zwischen den Reihen" + ${contentController.field(...controllerBaseKeyPath, 'gridRowGap')} + select + ></x-buildify-spacing-picker> + + <x-checkbox + .label=${'Anordnung der Items im Grid umdrehen'} + ${contentController.field(...controllerBaseKeyPath, 'revertDirection')} + ></x-checkbox> + + ${content.tableView + ? html` + <x-infobox> Tabellenoptik </x-infobox> + <x-checkbox + .label=${'Horizontale Linien hinzufügen'} + ${contentController.field(...controllerBaseKeyPath, 'tableView', 'hasHorizontalSeperator')} + ></x-checkbox> + <x-checkbox + .label=${'Vertikale Linien hinzufügen'} + ${contentController.field(...controllerBaseKeyPath, 'tableView', 'hasVerticalSeperator')} + ></x-checkbox> + <x-buildify-spacing-picker + placeholder="Breite der Linien" + ${contentController.field(...controllerBaseKeyPath, 'tableView', 'thickness')} + ></x-buildify-spacing-picker> + <x-buildify-color-picker + placeholder="Farbe der Linien" + ${contentController.field(...controllerBaseKeyPath, 'tableView', 'colorSeperators')} + ></x-buildify-color-picker> + <x-icon-button + icon="delete" + text="Tabellenoptik entfernen" + @click=${() => { + content.tableView = null; + host.requestUpdate(); + }} + ></x-icon-button> + ` + : html` + <x-icon-button + icon="add" + text="Tabellenoptik hinzufügen" + @click=${() => { + content.tableView = new HTMLBaseContainerGridTableView({}); + host.requestUpdate(); + }} + ></x-icon-button> + `} + ${ContainerEditor({ + content, + // @ts-expect-error its the same class + contentController, + controllerBaseKeyPath, + host, + })} + </x-flex> + `; +}; diff --git a/lab/html-based-buildify/client/nodes/grid/grid-cell/config.ts b/lab/html-based-buildify/client/nodes/grid/grid-cell/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..b0f476dedc2b5fc6ba535fb20c03511fba85eaa3 --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/grid-cell/config.ts @@ -0,0 +1,13 @@ +import type { Schema } from 'prosemirror-model'; +import { GRID_CELL_KEY, GRID_CELL_SCHEMA } from '../../../../schema/nodes/grid.js'; +import type { INodeConfig } from '../../../types.js'; +import './node-grid-cell.js'; +import { gridCellViewPlugin } from './node-view.js'; + +export const gridCell: INodeConfig = { + node: { + name: GRID_CELL_KEY, + schema: GRID_CELL_SCHEMA, + }, + plugins: (schema: Schema) => [gridCellViewPlugin], +}; diff --git a/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts b/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts new file mode 100644 index 0000000000000000000000000000000000000000..35e340b7648c0cd8a51a9d564808e732b3098a34 --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/grid-cell/node-grid-cell.ts @@ -0,0 +1,152 @@ +import type { Styles } from '@adornis/ass/style.js'; +import { ModeConsumer } from '@adornis/buildify/client/globals/consumer.js'; +import { ChemistryLitElement } from '@adornis/chemistry/chemistry-lit-element.js'; +import { html, type PropertyValues } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { EditorView } from 'prosemirror-view'; +import { updateNodeAttrs } from '../../../util.js'; +import type { NodeGrid } from '../node-grid.js'; + +@customElement('node-grid-cell') +export class NodeGridCell extends ChemistryLitElement { + private readonly _consumedMode = ModeConsumer(this); + + @property({ attribute: false }) view?: EditorView; + @property({ attribute: false }) getPos?: () => number | undefined; + + @property({ attribute: false }) thickness!: string; + @property({ attribute: false }) color!: string; + @property({ attribute: false }) rowGap!: number; + @property({ attribute: false }) columnGap!: number; + + override updated(changedProperties: PropertyValues): void { + super.updated(changedProperties); + + const grid = this.parentElement as NodeGrid; + if (!grid) return; + const gridData = grid._constructedData.value; + const tableView = gridData?.tableView; + if (!tableView) return; + this.thickness = tableView.thickness ?? '1px'; + this.color = tableView.colorSeperators ?? '#333'; + + const gridColumnGap = gridData.gridColumnGap; + const parsedGridColumnGap = Number.parseInt(gridColumnGap?.split('px')[0] ?? ''); + this.columnGap = parsedGridColumnGap; + + const gridRowGap = gridData.gridRowGap; + const parsedGridRowGap = Number.parseInt(gridRowGap?.split('px')[0] ?? ''); + this.rowGap = parsedGridRowGap; + + if (!this.view || !this.getPos) return; + + const classList = ['grid-item']; + // 'children' enthält alle direkten Kind-Elemente des Elternteils + const childrenArray = Array.from(grid.children); + + // Bestimmen des Index des 'childElement' innerhalb der 'childrenArray' + const index = childrenArray.indexOf(this); + const columns = gridData.columns ?? 3; + const rows = Math.ceil(childrenArray.length / columns); + + if (index < rows * columns - columns && tableView.hasHorizontalSeperator) { + const column = index % columns; + if (column === 0) classList.push('line-below-max-right'); + else if (column === columns - 1) classList.push('line-below-max-left'); + else { + classList.push('line-below-max-right'); + classList.push('line-below-max-left'); + } + classList.push('grid-line-below'); + } + if (index % columns !== 0 && tableView.hasVerticalSeperator) { + const row = Math.floor(index / columns); + if (rows > 1) { + if (row === 0) classList.push('line-left-max-bottom'); + else if (row === rows - 1) classList.push('line-left-max-top'); + else { + classList.push('line-left-max-bottom'); + classList.push('line-left-max-top'); + } + } + classList.push('grid-line-left'); + } + + if (this.classList.length === classList.length && classList.every(item => this.classList.contains(item))) return; + + const { state, dispatch } = this.view; + const { tr } = state; + updateNodeAttrs( + state, + tr, + () => { + return this.getPos!() ?? 0; + }, + { class: classList.join(' ') }, + ); + dispatch(tr); + } + + override render() { + return html` <slot></slot> `; + } + + isEditMode() { + return this._consumedMode.value === 'edit'; + } + + override styles() { + return [ + ...super.styles(), + { + ':host': { + border: this.isEditMode() ? '1px solid grey' : '', + position: 'relative', + }, + ':host(.grid-item)': { + position: 'relative', + overflow: 'visible', + }, + ':host(.grid-item) > *': { + width: '100%', + }, + ':host(.grid-item.child-container-item) > :first-child': { + boxSizing: 'border-box', + height: '100%', + }, + ':host(.grid-line-left)::before': { + content: "''", + position: 'absolute', + left: this.columnGap ? `-${this.columnGap / 2}px` : '0', + top: '0', + bottom: '0', + width: this.thickness, + background: this.color, + zIndex: '10', + }, + ':host(.grid-line-below)::after': { + content: "''", + position: 'absolute', + bottom: this.rowGap ? `-${this.rowGap / 2}px` : '0', + height: this.thickness, + background: this.color, + left: '0', + right: '0', + zIndex: '10', + }, + ':host(.grid-line-left.line-left-max-bottom)::before': { + bottom: this.rowGap ? `-${this.rowGap / 2}px` : '0', + }, + ':host(.grid-line-left.line-left-max-top)::before': { + top: this.rowGap ? `-${this.rowGap / 2}px` : '0', + }, + ':host(.grid-line-below.line-below-max-left)::after': { + left: this.columnGap ? `-${this.columnGap / 2}px` : '0', + }, + ':host(.grid-line-below.line-below-max-right)::after': { + right: this.columnGap ? `-${this.columnGap / 2}px` : '0', + }, + }, + ] as Styles[]; + } +} diff --git a/lab/html-based-buildify/client/nodes/grid/grid-cell/node-view.ts b/lab/html-based-buildify/client/nodes/grid/grid-cell/node-view.ts new file mode 100644 index 0000000000000000000000000000000000000000..60702f306c5d482d56829222c302dcf7a8f531cd --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/grid-cell/node-view.ts @@ -0,0 +1,53 @@ +import { Node as ProsemirrorNode } from 'prosemirror-model'; +import { Plugin, PluginKey } from 'prosemirror-state'; +import type { EditorView, NodeViewConstructor } from 'prosemirror-view'; +import { GRID_CELL_KEY } from '../../../../schema/nodes/grid.js'; +import type { NodeGridCell } from './node-grid-cell.js'; + +class GridCellNodeView { + view: EditorView; + dom: HTMLElement; + contentDOM: HTMLElement; + node: ProsemirrorNode; + + constructor(node: ProsemirrorNode, view: EditorView, getPos: () => number | undefined) { + this.node = node; + this.view = view; + + const cell = document.createElement(`node-grid-cell`) as NodeGridCell; + cell.setAttribute('class', node.attrs.class); + cell.view = view; + cell.getPos = getPos; + this.dom = cell; + this.contentDOM = cell; + } + + update(node: ProsemirrorNode): boolean { + if (node.attrs.data !== this.node.attrs.data) return false; + this.node = node; + this.dom.setAttribute('class', node.attrs.class); + return true; + } + + destroy(): void { + // @ts-expect-error still works + this.dom = null; + } +} + +const gridCellNodeView: NodeViewConstructor = ( + node: ProsemirrorNode, + view: EditorView, + getPos: () => number | undefined, +) => { + return new GridCellNodeView(node, view, getPos); +}; + +export const gridCellViewPlugin = new Plugin({ + key: new PluginKey('grid-cell-nodeview-plugin'), + props: { + nodeViews: { + [GRID_CELL_KEY]: gridCellNodeView, + }, + }, +}); diff --git a/lab/html-based-buildify/client/nodes/grid/node-grid.ts b/lab/html-based-buildify/client/nodes/grid/node-grid.ts new file mode 100644 index 0000000000000000000000000000000000000000..6d16385fda3ddca0a638263c7a4758cfa58459b8 --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/node-grid.ts @@ -0,0 +1,68 @@ +import type { Styles } from '@adornis/ass/style.js'; +import { css } from '@adornis/chemistry/directives/css.js'; +import { html, nothing } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { Node as ProsemirrorNode } from 'prosemirror-model'; +import type { EditorView } from 'prosemirror-view'; +import type { HTMLBaseContainerGrid } from '../../../db/HTMLBaseContainerGrid.js'; +import { BuildifyLitElement } from '../../BuildifyLitElement.js'; + +@customElement('node-grid') +export class NodeGrid extends BuildifyLitElement<HTMLBaseContainerGrid> { + @property({ attribute: false }) view?: EditorView; + @property({ attribute: false }) node?: ProsemirrorNode; + @property({ attribute: false }) getPos?: () => number | undefined; + + override content({ content, mode }: { content?: HTMLBaseContainerGrid; mode }) { + const columns = content?.columns ? content.columns : 3; + const templateColumns: string[] = []; + for (let i = 0; i < columns; i++) { + const templateValue = content?.getColumnWidthByIndex(i) ?? '1fr'; + templateColumns.push(templateValue); + } + + return html` + <x-grid + columns=${templateColumns.join(' ')} + ${css({ + gridColumnGap: content?.gridColumnGap ?? '', + gridRowGap: content?.gridRowGap ?? '', + padding: content?.padding ?? '', + background: content?.backgroundColor ?? '', + })} + > + <slot></slot> + ${mode === 'edit' + ? html` + <x-flex + ${css({ border: '1px lightgrey dashed', userSelect: 'none', cursor: 'pointer' })} + center + crossaxis-center + @click=${e => { + e.preventDefault(); + e.stopPropagation(); + this.append((document.createElement('node-grid-cell').innerText = 'Hier tippen...')); + }} + > + Zelle hinzufügen + </x-flex> + ` + : nothing} + </x-grid> + `; + } + + override styles() { + return [ + ...super.styles(), + { + ':host': { + borderRadius: this._data.value?.borderRadius ?? '', + }, + 'x-grid>*::slotted(*)': { + boxSizing: 'border-box', + }, + }, + ] as Styles[]; + } +} diff --git a/lab/html-based-buildify/client/nodes/grid/node-view.ts b/lab/html-based-buildify/client/nodes/grid/node-view.ts new file mode 100644 index 0000000000000000000000000000000000000000..75173191c0c5e27c0cebba45e199ef8754cede0b --- /dev/null +++ b/lab/html-based-buildify/client/nodes/grid/node-view.ts @@ -0,0 +1,59 @@ +// import { Node as ProsemirrorNode } from 'prosemirror-model'; +// import { Plugin, PluginKey } from 'prosemirror-state'; +// import type { Decoration, DecorationSource, EditorView, NodeViewConstructor } from 'prosemirror-view'; +// import { GRID_KEY } from '../../../schema/nodes/grid.js'; +// import type { NodeGrid } from './node-grid.js'; + +// class GridNodeView { +// view: EditorView; +// dom: HTMLElement; +// contentDOM: HTMLElement; +// node: ProsemirrorNode; + +// constructor( +// node: ProsemirrorNode, +// view: EditorView, +// getPos: () => number | undefined, +// decorations: readonly Decoration[], +// innerDecorations: DecorationSource, +// ) { +// this.node = node; +// this.view = view; + +// const nodeGrid = document.createElement(`node-grid`) as NodeGrid; +// nodeGrid.setAttribute('data', node.attrs.data); +// this.dom = nodeGrid; +// this.contentDOM = nodeGrid; +// } + +// update(node: ProsemirrorNode): boolean { +// if (node.attrs.data !== this.node.attrs.data) return false; +// this.node = node; +// this.dom.setAttribute('data', node.attrs.data); +// return true; +// } + +// destroy(): void { +// // @ts-expect-error still works +// this.dom = null; +// } +// } + +// const gridNodeView: NodeViewConstructor = ( +// node: ProsemirrorNode, +// view: EditorView, +// getPos: () => number | undefined, +// decorations: readonly Decoration[], +// innerDecorations: DecorationSource, +// ) => { +// return new GridNodeView(node, view, getPos, decorations, innerDecorations); +// }; + +// export const gridViewPlugin = new Plugin({ +// key: new PluginKey('grid-nodeview-plugin'), +// props: { +// nodeViews: { +// [GRID_KEY]: gridNodeView, +// }, +// }, +// }); diff --git a/lab/html-based-buildify/client/nodes/iframe.ts b/lab/html-based-buildify/client/nodes/iframe.ts new file mode 100644 index 0000000000000000000000000000000000000000..97542eea32206069cb4261989f05453a4ae5bd60 --- /dev/null +++ b/lab/html-based-buildify/client/nodes/iframe.ts @@ -0,0 +1,49 @@ +import '@adornis/buildify/client/components/x-buildify-color-picker'; +import '@adornis/buildify/client/components/x-buildify-spacing-picker'; +import { css } from '@adornis/chemistry/directives/css.js'; +import '@adornis/chemistry/elements/components/x-flex'; +import '@adornis/chemistry/elements/components/x-icon'; +import '@adornis/forms/x-checkbox.js'; +import '@adornis/forms/x-input'; +import { html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import type { HTMLBaseIframe } from '../../db/HTMLBaseIframe.js'; +import { IFRAME_KEY, IFRAME_SCHEMA } from '../../schema/nodes/iframe.js'; +import { BuildifyLitElement } from '../BuildifyLitElement.js'; +import { runInsertNode } from '../helper/runInsertNode.js'; +import { type EditorFunc, type INodeConfig } from '../types.js'; +import { isNodeSelection } from '../util.js'; + +export const editor: EditorFunc<HTMLBaseIframe> = ({ content, contentController, controllerBaseKeyPath, host }) => { + return html` + <x-flex space="md"> + <x-input placeholder="Url*" ${contentController.field(...controllerBaseKeyPath, 'url')}></x-input> + <x-input placeholder="Höhe" ${contentController.field(...controllerBaseKeyPath, 'height')}></x-input> + </x-flex> + `; +}; + +export const iframe: INodeConfig = { + node: { + name: IFRAME_KEY, + schema: IFRAME_SCHEMA, + }, + menuItems: [ + { + label: 'iFrame', + icon: 'frame_inspect', + run: view => runInsertNode(IFRAME_KEY)(view.state, view), + shouldVisualize: view => { + return !isNodeSelection(view.state.selection); + }, + }, + ], + editor, +}; + +@customElement('node-iframe') +export class NodeIframe extends BuildifyLitElement<HTMLBaseIframe> { + override content({ content, mode }) { + return html` <iframe ${css({ width: '100%', height: content.height ?? '500px' })} src=${content.url}></iframe> `; + } +} diff --git a/lab/html-based-buildify/client/prosemirror-editor.ts b/lab/html-based-buildify/client/prosemirror-editor.ts index 596801ef4860670528e082ec4b8b8ea0dd267272..c011f8bb32aca954ce198a938a4f16532a156263 100644 --- a/lab/html-based-buildify/client/prosemirror-editor.ts +++ b/lab/html-based-buildify/client/prosemirror-editor.ts @@ -27,7 +27,10 @@ import { EditorView } from 'prosemirror-view'; import { combineLatest, debounceTime, distinctUntilChanged, filter, switchMap, takeUntil } from 'rxjs'; import { Contexts, Size } from '../db/enums.js'; import { colorMenuItem } from './marks/color.js'; +import { createFontSizeMenuItem } from './marks/font-size.js'; import { createTextAlignMenuItem } from './marks/text-align.js'; +import { CollapsibleMenuItem } from './menu-items/CollapsibleMenuItem.js'; +import { CopySelectionMenuItem, PasteFromClipboardMenuItem } from './menu-items/CopySelectionToClipboard.js'; import { DeleteSelectedNodeMenuItem } from './menu-items/DeleteSelectedNode.js'; import { EditGlobalSettingsMenuItem } from './menu-items/EditGlobalSettings.js'; import { EditSelectedNodeMenuItem } from './menu-items/EditSelectedNode.js'; @@ -69,6 +72,16 @@ export class ProsemirrorEditor extends FormField<string> { createTextAlignMenuItem('center'), createTextAlignMenuItem('right'), createTextAlignMenuItem('justify'), + new CollapsibleMenuItem({ + label: 'Schriftgröße', + icon: 'text_fields', + menuItems: [ + createFontSizeMenuItem('8px'), + createFontSizeMenuItem('12px'), + createFontSizeMenuItem('16px'), + createFontSizeMenuItem('20px'), + ], + }), ]); @property({ attribute: false }) globalSettingsClassName = new RXController(this, BuildifyGlobalSettings._class); @@ -196,6 +209,8 @@ export class ProsemirrorEditor extends FormField<string> { menuItems.push(EditSelectedNodeMenuItem(this.getEditors(), this.globalSettingsClassName.value)); menuItems.push(DeleteSelectedNodeMenuItem); menuItems.push(EditGlobalSettingsMenuItem(this.globalSettingsClassName.value)); + menuItems.push(CopySelectionMenuItem); + menuItems.push(PasteFromClipboardMenuItem); return menuItems; } @@ -219,13 +234,11 @@ export class ProsemirrorEditor extends FormField<string> { .pipe(filter(Boolean), distinctUntilChanged()) .subscribe(globalSettingsClassName => { this._globalSettingsClassNameProvider.setValue(globalSettingsClassName, true); - console.log('global settings classname changed!'); }); combineLatest([this.configs.observable, this.additionalMenuItems.observable, this._globalSettings.observable]) .pipe(debounceTime(300)) .subscribe(([configs, additionalMenuItems, globalSettings]) => { - console.log('call update editor'); this.updateEditor(); }); diff --git a/lab/html-based-buildify/client/starterPack.ts b/lab/html-based-buildify/client/starterPack.ts index 8aae97fac45468e84ea9923ebc56577cf333247f..e641282169d6c33794a0b57370c0c7a59ba370c7 100644 --- a/lab/html-based-buildify/client/starterPack.ts +++ b/lab/html-based-buildify/client/starterPack.ts @@ -11,11 +11,13 @@ import { doc } from './nodes/doc.js'; import { excalidraw } from './nodes/excalidraw.js'; import { file } from './nodes/file.js'; import { flex } from './nodes/flex.js'; -import { grid, gridCell } from './nodes/grid.js'; +import { grid } from './nodes/grid/config.js'; +import { gridCell } from './nodes/grid/grid-cell/config.js'; import { hardBreak } from './nodes/hard-break.js'; import { heading } from './nodes/headings.js'; import { icon } from './nodes/icon.js'; import { iconText, iconWrapper, textWrapper } from './nodes/iconText.js'; +import { iframe } from './nodes/iframe.js'; import { image } from './nodes/image.js'; import { bulletList, listItem, orderedList } from './nodes/list.js'; import { paragraph } from './nodes/paragraph.js'; @@ -63,4 +65,5 @@ export const startedPack: Array<INodeConfig | IMarkConfig> = [ tag, vimeo, youtube, + iframe, ]; diff --git a/lab/html-based-buildify/client/types.ts b/lab/html-based-buildify/client/types.ts index 02e7ed1191f3b6527313b00c6dbc5c36ec4f5ba7..060302bac2170a280ae28f32c9730aa4611274a8 100644 --- a/lab/html-based-buildify/client/types.ts +++ b/lab/html-based-buildify/client/types.ts @@ -31,6 +31,7 @@ export interface IMenuItem { run: (editorView: EditorView) => void; isActive?: (editorView: EditorView) => boolean; shouldVisualize?: (editorView: EditorView) => boolean; + render?: (editorView: EditorView) => TemplateResult; } export interface IEditorConfigBase { diff --git a/lab/html-based-buildify/client/x-prosemirror-toolbar.ts b/lab/html-based-buildify/client/x-prosemirror-toolbar.ts index 67876ddd6c444306a43bdb1eace86dbe5c30391e..77175edae484a6939117b906063630b0b2b948d1 100644 --- a/lab/html-based-buildify/client/x-prosemirror-toolbar.ts +++ b/lab/html-based-buildify/client/x-prosemirror-toolbar.ts @@ -1,14 +1,94 @@ import type { Styles } from '@adornis/ass/style.js'; import { ChemistryLitElement } from '@adornis/chemistry/chemistry-lit-element.js'; +import { acss } from '@adornis/chemistry/directives/acss.js'; import { css } from '@adornis/chemistry/directives/css.js'; import '@adornis/chemistry/elements/components/x-flex'; import '@adornis/chemistry/elements/components/x-icon'; +import '@adornis/fonts/fonts'; import '@adornis/popover/x-tooltip'; import { html, nothing, type TemplateResult } from 'lit'; import { customElement, property } from 'lit/decorators.js'; import { EditorView } from 'prosemirror-view'; import type { IMenuItem } from './types.js'; +export function renderMenuItemIcon({ + view, + isActive, + icon, + run, + color, +}: { + view: EditorView; + isActive: boolean; + icon: string; + run: IMenuItem['run']; + color?: IMenuItem['color']; +}) { + return html` + <x-icon + ${css({ + fontSize: '28px', + cursor: 'pointer', + padding: '2px', + background: isActive ? '#F2F2F1' : 'transparent', + borderRadius: '4px', + color: color ?? '#333', + })} + @click=${() => { + if (!run) return; + run(view); + }} + > + ${icon} + </x-icon> + `; +} + +export function renderMenuItem(item: IMenuItem, view: EditorView): TemplateResult | typeof nothing { + if (item.shouldVisualize && !item.shouldVisualize(view)) return nothing; + if (item.render) + return html` + <div + @click=${() => { + if (!item.run) return; + item.run(view); + }} + > + ${item.render(view)} + </div> + `; + + const isActive = item.isActive?.(view) ?? false; + + return html` + <x-flex> + ${item.label ? html` <x-tooltip .text=${item.label}></x-tooltip> ` : nothing} + ${item.icon + ? renderMenuItemIcon({ view, isActive, icon: item.icon, run: item.run, color: item.color }) + : html` + <x-flex + ${acss({ + cursor: 'pointer', + background: isActive ? '#4b6584' : '', + color: isActive ? '#fff' : '', + padding: '8px', + borderRadius: '4px', + '&:hover': { + background: isActive ? '' : '#F2F2F1', + }, + })} + @click=${() => { + if (!item.run) return; + item.run(view); + }} + > + <x-text> ${item.label} </x-text> + </x-flex> + `} + </x-flex> + `; +} + @customElement('x-prosemirror-toolbar') export class XProsemirrorToolbar extends ChemistryLitElement { @property({ attribute: false }) menuItems: IMenuItem[] = []; @@ -31,61 +111,18 @@ export class XProsemirrorToolbar extends ChemistryLitElement { for (const group of groups) { const items = this._getAllMenuItemsForGroup(group); for (const item of items) { - result.push(this._renderMenuItem(item, view)); + result.push(renderMenuItem(item, view)); } } // append other items const notGroupedItems = this.menuItems.filter(item => !item.group || !groups.includes(item.group)); for (const item of notGroupedItems) { - result.push(this._renderMenuItem(item, view)); + result.push(renderMenuItem(item, view)); } return result; } - private _renderMenuItem(item: IMenuItem, view: EditorView) { - if (item.shouldVisualize && !item.shouldVisualize(view)) return nothing; - - const isActive = item.isActive?.(view); - const style = isActive ? 'background: #4b6584; color: #fff;' : ''; - - return html` - <x-flex> - ${item.label ? html` <x-tooltip .text=${item.label}></x-tooltip> ` : nothing} - ${item.icon - ? html` - <x-icon - ${css({ - fontSize: '28px', - cursor: 'pointer', - padding: '2px', - background: isActive ? '#F2F2F1' : 'transparent', - borderRadius: '4px', - color: item.color ?? '#333', - })} - @click=${() => { - if (!item.run) return; - item.run(view); - }} - > - ${item.icon} - </x-icon> - ` - : html` - <button - style=${style} - @click=${() => { - if (!item.run) return; - item.run(view); - }} - > - ${item.label} - </button> - `} - </x-flex> - `; - } - private _getAllMenuItemsForGroup(group?: string) { return this.menuItems.filter(item => item.group === group); } diff --git a/lab/html-based-buildify/db/HTMLBaseIframe.ts b/lab/html-based-buildify/db/HTMLBaseIframe.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d6dd10230c437b3a2979407adbd582f1bbd7507 --- /dev/null +++ b/lab/html-based-buildify/db/HTMLBaseIframe.ts @@ -0,0 +1,16 @@ +import { Entity, Field } from '@adornis/baseql/decorators.js'; +import { validate } from '@adornis/validation/decorators.js'; +import { nonOptional } from '@adornis/validation/functions/nonOptional.js'; +import { HTMLBase } from './HTMLBase.js'; + +@Entity() +export class HTMLBaseIframe extends HTMLBase { + static override _class = 'HTMLBaseIframe'; + + @validate(nonOptional()) + @Field(type => String) + url!: string; + + @Field(type => String) + height!: string; +} diff --git a/lab/html-based-buildify/schema/nodes/grid.ts b/lab/html-based-buildify/schema/nodes/grid.ts index b9474f4c4ddd5d5e979fd8df2736ea838eed0ef7..79aab96b9459d69b6ef0166b750bd58c832a5c6b 100644 --- a/lab/html-based-buildify/schema/nodes/grid.ts +++ b/lab/html-based-buildify/schema/nodes/grid.ts @@ -1,9 +1,11 @@ +import { html } from 'lit'; import type { NodeSpec } from 'prosemirror-model'; +import { toDOMByTemplate } from '../../client/util.js'; import { HTMLBaseContainerGrid } from '../../db/HTMLBaseContainerGrid.js'; export const GRID_CELL_SCHEMA: NodeSpec = { attrs: { - class: { default: null }, + class: { default: '' }, }, content: 'block+', group: 'gridCell', @@ -46,7 +48,7 @@ export const GRID_SCHEMA: NodeSpec = { }, ], toDOM: node => { - return ['node-grid', { data: node.attrs.data }, 0]; + return toDOMByTemplate(html` <node-grid data=${node.attrs.data}></node-grid> `); }, }; diff --git a/lab/html-based-buildify/schema/nodes/iframe.ts b/lab/html-based-buildify/schema/nodes/iframe.ts new file mode 100644 index 0000000000000000000000000000000000000000..e53faa05b2c5014fa06495137372deaf374c9df2 --- /dev/null +++ b/lab/html-based-buildify/schema/nodes/iframe.ts @@ -0,0 +1,30 @@ +import type { NodeSpec } from 'prosemirror-model'; +import { HTMLBaseIframe } from '../../db/HTMLBaseIframe.js'; + +export const IFRAME_SCHEMA: NodeSpec = { + draggable: true, + atom: true, + isolating: true, + group: 'block', + attrs: { + data: { default: JSON.stringify(new HTMLBaseIframe({}).toObject()) }, + }, + parseDOM: [ + { + tag: 'node-iframe', + getAttrs(dom: HTMLElement) { + return { + data: dom.getAttribute('data'), + }; + }, + }, + ], + toDOM: node => { + return ['node-iframe', { data: node.attrs.data, 'no-content': 'true' }]; + }, +}; + +export const IFRAME_KEY = 'iframe'; +export const IFRAME = { + [IFRAME_KEY]: IFRAME_SCHEMA, +}; diff --git a/lab/wiki/client/x-wiki-editor.ts b/lab/wiki/client/x-wiki-editor.ts index 240b4120b6914378946f1859a0b017c708434655..c81da515e296a66217700cf8a91657d2d76820a9 100644 --- a/lab/wiki/client/x-wiki-editor.ts +++ b/lab/wiki/client/x-wiki-editor.ts @@ -710,6 +710,7 @@ export class XWikiEditor extends ChemistryLitElement { .value=${this._entry.value?.content} .configs=${[ ...startedPack, + ...this.additionalConfigs, embedding( (this._isDraft() ? (this._entry.value as Maybe<WikiEntryDraft>)?.referenceID : this._entry.value?._id) ?? '',