diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index f52122d007..bd52d7f096 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -17874,9 +17874,9 @@ } }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index 59a3af5f4c..cc2a60b22b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -17,6 +17,7 @@ export * from './input-checkbox-list/index.js'; export * from './input-color/index.js'; export * from './input-eye-dropper/index.js'; export * from './input-list-base/index.js'; +export * from './input-markdown-editor/index.js'; export * from './input-multi-url/index.js'; export * from './input-tiny-mce/index.js'; export * from './input-number-range/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/index.ts new file mode 100644 index 0000000000..073c23b3cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/index.ts @@ -0,0 +1 @@ +export * from './input-markdown.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts new file mode 100644 index 0000000000..e9202df63b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts @@ -0,0 +1,81 @@ +import { UmbCodeEditorElement, loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; +import { css, html, customElement, query, property } from '@umbraco-cms/backoffice/external/lit'; +import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; + +/** + * @element umb-input-markdown + * @fires change - when the value of the input changes + */ + +@customElement('umb-input-markdown') +export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { + protected getFormElement() { + return undefined; + } + + @property({ type: Boolean }) + preview?: boolean; + + #isCodeEditorReady = new UmbBooleanState(false); + isCodeEditorReady = this.#isCodeEditorReady.asObservable(); + + @query('umb-code-editor') + _codeEditor?: UmbCodeEditorElement; + + constructor() { + super(); + this.#loadCodeEditor(); + } + + async #loadCodeEditor() { + try { + await loadCodeEditor(); + this._codeEditor?.editor?.updateOptions({ + lineNumbers: false, + minimap: false, + folding: false, + }); + this.#isCodeEditorReady.next(true); + } catch (error) { + console.error(error); + } + } + + render() { + return html`
+ + ${this.renderPreview()}`; + } + + renderPreview() { + if (!this.preview) return; + return html`
TODO Preview
`; + } + + static styles = [ + css` + :host { + display: flex; + flex-direction: column; + } + #actions { + background-color: var(--uui-color-background-alt); + display: flex; + } + + umb-code-editor { + height: 200px; + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-divider-emphasis); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-markdown': UmbInputMarkdownElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.stories.ts new file mode 100644 index 0000000000..01056abb10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.stories.ts @@ -0,0 +1,13 @@ +import { Meta, StoryObj } from '@storybook/web-components'; +import './input-markdown.element.js'; +import type { UmbInputMarkdownElement } from './input-markdown.element.js'; + +const meta: Meta = { + title: 'Components/Inputs/Markdown', + component: 'umb-input-markdown', +}; + +export default meta; +type Story = StoryObj; + +export const Overview: Story = {}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.element.ts index af33034ac8..74778acfd9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.element.ts @@ -3,6 +3,7 @@ import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; import { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import { UmbInputMarkdownElement } from '@umbraco-cms/backoffice/components'; /** * @element umb-property-editor-ui-markdown-editor @@ -15,11 +16,24 @@ export class UmbPropertyEditorUIMarkdownEditorElement @property() value = ''; + @state() + private _preview?: boolean; + @property({ attribute: false }) - public config?: UmbPropertyEditorConfigCollection; + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + this._preview = config?.getValueByAlias('preview'); + } + + #onChange(e: Event) { + this.value = (e.target as UmbInputMarkdownElement).value as string; + this.dispatchEvent(new CustomEvent('property-value-change')); + } render() { - return html`
umb-property-editor-ui-markdown-editor
`; + return html``; } static styles = [UmbTextStyles]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx new file mode 100644 index 0000000000..fdf53c52be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/sorter-controller.mdx @@ -0,0 +1,186 @@ +import { Canvas, Meta } from '@storybook/addon-docs'; + +import * as LocalizeStories from './sorter.stories'; + + + +# Drag and Drop + +Drag and Drop can be done by using the `UmbSorterController` + +To get started using drag and drop, finish the following steps: + +- Preparing the model +- Setting the configuration +- Registering the controller + +#### Preparing the model + +The SorterController needs a model to know what item it is we are dealing with. + +```typescript +type MySortEntryType = { + id: string; + value: string; +}; + +const awesomeModel: Array = [ + { + id: '0', + value: 'Entry 0', + }, + { + id: '1', + value: 'Entry 1', + }, + { + id: '2', + value: 'Entry 2', + }, +]; +``` + +#### Setting the configuration + +When you know the model of which that is being sorted, you can set up the configuration. +The configuration has a lot of optional options, but the required ones are: + +- compareElementToModel() +- querySelectModelToElement() +- identifier +- itemSelector +- containerSelector + +It can be set up as following: + +```typescript +import { UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +type MySortEntryType = {...} +const awesomeModel: Array = [...] + +const MY_SORTER_CONFIG: UmbSorterConfig = { + compareElementToModel: (element: HTMLElement, model: MySortEntryType) => { + return element.getAttribute('data-sort-entry-id') === model.id; + }, + querySelectModelToElement: (container: HTMLElement, modelEntry: MySortEntryType) => { + return container.querySelector('data-sort-entry-id=[' + modelEntry.id + ']'); + }, + identifier: 'test-sorter', + itemSelector: 'li', + containerSelector: 'ul', +}; + +export class MyElement extends UmbElementMixin(LitElement) { + + render() { + return html` +
    + ${awesomeModel.map( + (entry) => + html`
  • + ${entry.value} +
  • `, + )} +
+ `; + } +} +``` + +#### Registering the controller + +When the model and configuration are available we can register the controller and tell the controller what model we are using. + +```typescript +import { UmbSorterController, UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +type MySortEntryType = {...} +const awesomeModel: Array = [...] +const MY_SORTER_CONFIG: UmbSorterConfig = {...} + + +export class MyElement extends UmbElementMixin(LitElement) { + #sorter = new UmbSorterController(this, MY_SORTER_CONFIG); + + constructor() { + this.#sorter.setModel(awesomeModel); + } + + render() { + return html` +
    + ${awesomeModel.map( + (entry) => + html`
  • + ${entry.value} +
  • `, + )} +
+ `; + } +} +``` + +### Placeholder + +While dragging an entry, the entry will get an additional class that can be styled. +The class is by default `--umb-sorter-placeholder` but can be changed via the configuration to a different value. + +```typescript +const MY_SORTER_CONFIG: UmbSorterConfig = { + ... + placeholderClass: 'dragging-now', +}; +``` + +```typescript + static styles = [ + css` + li { + display:relative; + } + + li.dragging-now span { + visibility: hidden; + } + + li.dragging-now::after { + content: ''; + position: absolute; + inset: 0px; + border: 1px dashed grey; + } + `, +]; +``` + +### Horizontal sorting + +By default, the sorter controller will sort vertically. You can sort your model horizontally by setting the `resolveVerticalDirection` to return false. + +```typescript +const MY_SORTER_CONFIG: UmbSorterConfig = { + ... + resolveVerticalDirection: () => return false, +}; +``` + +### Performing logic when using the controller (TODO: Better title) + +Let's say your model has a property sortOrder that you would like to update when the entry is being sorted. +You can add your code logic in the configuration option `performItemInsert` and `performItemRemove` + +```typescript +export class MyElement extends UmbElementMixin(LitElement) { + #sorter = new UmbSorterController(this, { + ...SORTER_CONFIG, + performItemInsert: ({ item, newIndex }) => { + console.log(item, newIndex); + // Perform some logic here to calculate the new sortOrder & save it. + return true; + }, + performItemRemove: () => true, + }); +} +``` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts index c6843e234b..6ad3b41ad1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/stories/test-sorter-controller.element.ts @@ -1,24 +1,23 @@ import { UmbSorterConfig, UmbSorterController } from '../sorter.controller.js'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; -import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; type SortEntryType = { id: string; value: string; }; -const sorterConfig: UmbSorterConfig = { +const SORTER_CONFIG: UmbSorterConfig = { compareElementToModel: (element: HTMLElement, model: SortEntryType) => { return element.getAttribute('data-sort-entry-id') === model.id; }, querySelectModelToElement: (container: HTMLElement, modelEntry: SortEntryType) => { - return container.querySelector('data-sort-entry-id[' + modelEntry.id + ']'); + return container.querySelector('data-sort-entry-id=[' + modelEntry.id + ']'); }, identifier: 'test-sorter', itemSelector: 'li', containerSelector: 'ul', }; - const model: Array = [ { id: '0', @@ -38,18 +37,35 @@ const model: Array = [ export default class UmbTestSorterControllerElement extends UmbLitElement { public sorter; + @state() + private vertical = true; + constructor() { super(); - - this.sorter = new UmbSorterController(this, sorterConfig); + this.sorter = new UmbSorterController(this, { + ...SORTER_CONFIG, + resolveVerticalDirection: () => { + this.vertical ? true : false; + }, + }); this.sorter.setModel(model); } + #toggle() { + this.vertical = !this.vertical; + } + render() { return html` -
    + + Horizontal/Vertical + +
      ${model.map( - (entry) => html`
    • ${entry.value}
    • ` + (entry) => + html`
    • + ${entry.value} +
    • `, )}
    `; @@ -59,39 +75,47 @@ export default class UmbTestSorterControllerElement extends UmbLitElement { css` :host { display: block; + box-sizing: border-box; } ul { + display: flex; + flex-direction: column; + gap: 5px; list-style: none; padding: 0; - margin: 0; + margin: 10px 0; + } + + ul.horizontal { + flex-direction: row; } li { + cursor: grab; + position: relative; + flex: 1; + border-radius: var(--uui-border-radius); + } + + li span { + display: flex; + align-items: center; + gap: 5px; padding: 10px; - margin: 5px; - background: #eee; + background-color: rgba(0, 255, 0, 0.3); } - li:hover { - background: #ddd !important; - cursor: move; + li.--umb-sorter-placeholder span { + visibility: hidden; } - li:active { - background: #ccc; - } - - #sort0 { - background: #f00; - } - - #sort1 { - background: #0f0; - } - - #sort2 { - background: #c9da10; + li.--umb-sorter-placeholder::after { + content: ''; + position: absolute; + inset: 0px; + border-radius: var(--uui-border-radius); + border: 1px dashed var(--uui-color-divider-emphasis); } `, ];