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);
}
`,
];