mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-10-01 01:36:00 -04:00
Lexical: Added form complex/tab ui support
This commit is contained in:
parent
c8f6b7e0d6
commit
ce8c9dd079
@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
## In progress
|
## In progress
|
||||||
|
|
||||||
- Update forms to allow panels (Media)
|
//
|
||||||
- Will be used for table forms also.
|
|
||||||
|
|
||||||
## Main Todo
|
## Main Todo
|
||||||
|
|
||||||
@ -23,6 +22,7 @@
|
|||||||
- Image gallery integration for form
|
- Image gallery integration for form
|
||||||
- Drawing gallery integration
|
- Drawing gallery integration
|
||||||
- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
|
- Support media src conversions (https://github.com/tinymce/tinymce/blob/release/6.6/modules/tinymce/src/plugins/media/main/ts/core/UrlPatterns.ts)
|
||||||
|
- Media resize support (like images)
|
||||||
|
|
||||||
## Bugs
|
## Bugs
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {EditorFormDefinition, EditorSelectFormFieldDefinition} from "../framework/forms";
|
import {EditorFormDefinition, EditorFormTabs, EditorSelectFormFieldDefinition} from "../framework/forms";
|
||||||
import {EditorUiContext} from "../framework/core";
|
import {EditorUiContext} from "../framework/core";
|
||||||
import {$createLinkNode} from "@lexical/link";
|
import {$createLinkNode} from "@lexical/link";
|
||||||
import {$createTextNode, $getSelection, LexicalNode} from "lexical";
|
import {$createTextNode, $getSelection, LexicalNode} from "lexical";
|
||||||
@ -133,25 +133,40 @@ export const media: EditorFormDefinition = {
|
|||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
label: 'Source',
|
build() {
|
||||||
name: 'src',
|
return new EditorFormTabs([
|
||||||
type: 'text',
|
{
|
||||||
},
|
label: 'General',
|
||||||
{
|
contents: [
|
||||||
label: 'Width',
|
{
|
||||||
name: 'width',
|
label: 'Source',
|
||||||
type: 'text',
|
name: 'src',
|
||||||
},
|
type: 'text',
|
||||||
{
|
},
|
||||||
label: 'Height',
|
{
|
||||||
name: 'height',
|
label: 'Width',
|
||||||
type: 'text',
|
name: 'width',
|
||||||
},
|
type: 'text',
|
||||||
// TODO - Tabbed interface to separate this option
|
},
|
||||||
{
|
{
|
||||||
label: 'Paste your embed code below:',
|
label: 'Height',
|
||||||
name: 'embed',
|
name: 'height',
|
||||||
type: 'textarea',
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Embed',
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
label: 'Paste your embed code below:',
|
||||||
|
name: 'embed',
|
||||||
|
type: 'textarea',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -18,6 +18,14 @@ export type EditorUiContext = {
|
|||||||
options: Record<string, any>; // General user options which may be used by sub elements
|
options: Record<string, any>; // General user options which may be used by sub elements
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface EditorUiBuilderDefinition {
|
||||||
|
build: () => EditorUiElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUiBuilderDefinition(object: any): object is EditorUiBuilderDefinition {
|
||||||
|
return 'build' in object;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class EditorUiElement {
|
export abstract class EditorUiElement {
|
||||||
protected dom: HTMLElement|null = null;
|
protected dom: HTMLElement|null = null;
|
||||||
private context: EditorUiContext|null = null;
|
private context: EditorUiContext|null = null;
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import {EditorUiContext, EditorUiElement, EditorContainerUiElement} from "./core";
|
import {
|
||||||
|
EditorUiContext,
|
||||||
|
EditorUiElement,
|
||||||
|
EditorContainerUiElement,
|
||||||
|
EditorUiBuilderDefinition,
|
||||||
|
isUiBuilderDefinition
|
||||||
|
} from "./core";
|
||||||
import {el} from "../../helpers";
|
import {el} from "../../helpers";
|
||||||
|
import {uniqueId} from "../../../services/util";
|
||||||
|
|
||||||
export interface EditorFormFieldDefinition {
|
export interface EditorFormFieldDefinition {
|
||||||
label: string;
|
label: string;
|
||||||
@ -12,10 +19,15 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
|
|||||||
valuesByLabel: Record<string, string>
|
valuesByLabel: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditorFormTabDefinition {
|
||||||
|
label: string;
|
||||||
|
contents: EditorFormFieldDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditorFormDefinition {
|
export interface EditorFormDefinition {
|
||||||
submitText: string;
|
submitText: string;
|
||||||
action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
|
action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
|
||||||
fields: EditorFormFieldDefinition[];
|
fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditorFormField extends EditorUiElement {
|
export class EditorFormField extends EditorUiElement {
|
||||||
@ -62,7 +74,14 @@ export class EditorForm extends EditorContainerUiElement {
|
|||||||
protected onCancel: null|(() => void) = null;
|
protected onCancel: null|(() => void) = null;
|
||||||
|
|
||||||
constructor(definition: EditorFormDefinition) {
|
constructor(definition: EditorFormDefinition) {
|
||||||
super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition)));
|
let children: (EditorFormField|EditorUiElement)[] = definition.fields.map(fieldDefinition => {
|
||||||
|
if (isUiBuilderDefinition(fieldDefinition)) {
|
||||||
|
return fieldDefinition.build();
|
||||||
|
}
|
||||||
|
return new EditorFormField(fieldDefinition)
|
||||||
|
});
|
||||||
|
|
||||||
|
super(children);
|
||||||
this.definition = definition;
|
this.definition = definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,13 +99,23 @@ export class EditorForm extends EditorContainerUiElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getFieldByName(name: string): EditorFormField|null {
|
protected getFieldByName(name: string): EditorFormField|null {
|
||||||
for (const child of this.children as EditorFormField[]) {
|
|
||||||
if (child.getName() === name) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
const search = (children: EditorUiElement[]): EditorFormField|null => {
|
||||||
|
for (const child of children) {
|
||||||
|
if (child instanceof EditorFormField && child.getName() === name) {
|
||||||
|
return child;
|
||||||
|
} else if (child instanceof EditorContainerUiElement) {
|
||||||
|
const matchingChild = search(child.getChildren());
|
||||||
|
if (matchingChild) {
|
||||||
|
return matchingChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return search(this.getChildren());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildDOM(): HTMLElement {
|
protected buildDOM(): HTMLElement {
|
||||||
@ -113,4 +142,91 @@ export class EditorForm extends EditorContainerUiElement {
|
|||||||
|
|
||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EditorFormTab extends EditorContainerUiElement {
|
||||||
|
|
||||||
|
protected definition: EditorFormTabDefinition;
|
||||||
|
protected fields: EditorFormField[];
|
||||||
|
protected id: string;
|
||||||
|
|
||||||
|
constructor(definition: EditorFormTabDefinition) {
|
||||||
|
const fields = definition.contents.map(fieldDef => new EditorFormField(fieldDef));
|
||||||
|
super(fields);
|
||||||
|
|
||||||
|
this.definition = definition;
|
||||||
|
this.fields = fields;
|
||||||
|
this.id = uniqueId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLabel(): string {
|
||||||
|
return this.getContext().translate(this.definition.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getId(): string {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildDOM(): HTMLElement {
|
||||||
|
return el(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
class: 'editor-form-tab-content',
|
||||||
|
role: 'tabpanel',
|
||||||
|
id: `editor-tabpanel-${this.id}`,
|
||||||
|
'aria-labelledby': `editor-tab-${this.id}`,
|
||||||
|
},
|
||||||
|
this.fields.map(f => f.getDOMElement())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class EditorFormTabs extends EditorContainerUiElement {
|
||||||
|
|
||||||
|
protected definitions: EditorFormTabDefinition[] = [];
|
||||||
|
protected tabs: EditorFormTab[] = [];
|
||||||
|
|
||||||
|
constructor(definitions: EditorFormTabDefinition[]) {
|
||||||
|
const tabs: EditorFormTab[] = definitions.map(d => new EditorFormTab(d));
|
||||||
|
super(tabs);
|
||||||
|
|
||||||
|
this.definitions = definitions;
|
||||||
|
this.tabs = tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildDOM(): HTMLElement {
|
||||||
|
const controls: HTMLElement[] = [];
|
||||||
|
const contents: HTMLElement[] = [];
|
||||||
|
|
||||||
|
const selectTab = (tabIndex: number) => {
|
||||||
|
for (let i = 0; i < controls.length; i++) {
|
||||||
|
controls[i].setAttribute('aria-selected', (i === tabIndex) ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
for (let i = 0; i < contents.length; i++) {
|
||||||
|
contents[i].hidden = !(i === tabIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const tab of this.tabs) {
|
||||||
|
const button = el('button', {
|
||||||
|
class: 'editor-form-tab-control',
|
||||||
|
type: 'button',
|
||||||
|
role: 'tab',
|
||||||
|
id: `editor-tab-${tab.getId()}`,
|
||||||
|
'aria-controls': `editor-tabpanel-${tab.getId()}`
|
||||||
|
}, [tab.getLabel()]);
|
||||||
|
contents.push(tab.getDOMElement());
|
||||||
|
controls.push(button);
|
||||||
|
|
||||||
|
button.addEventListener('click', event => {
|
||||||
|
selectTab(controls.indexOf(button));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectTab(0);
|
||||||
|
|
||||||
|
return el('div', {class: 'editor-form-tab-container'}, [
|
||||||
|
el('div', {class: 'editor-form-tab-controls'}, controls),
|
||||||
|
el('div', {class: 'editor-form-tab-contents'}, contents),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
@ -398,6 +398,45 @@ textarea.editor-form-field-input {
|
|||||||
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
|
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.editor-form-tab-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
.editor-form-tab-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: .25rem;
|
||||||
|
}
|
||||||
|
.editor-form-tab-control {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #444;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: .25rem .5rem;
|
||||||
|
text-align: start;
|
||||||
|
&[aria-selected="true"] {
|
||||||
|
border-color: var(--editor-color-primary);
|
||||||
|
color: var(--editor-color-primary);
|
||||||
|
}
|
||||||
|
&[aria-selected="true"]:after, &:hover:after {
|
||||||
|
background-color: var(--editor-color-primary);
|
||||||
|
opacity: .15;
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.editor-form-tab-contents {
|
||||||
|
width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
// Editor theme styles
|
// Editor theme styles
|
||||||
.editor-theme-bold {
|
.editor-theme-bold {
|
||||||
|
Loading…
Reference in New Issue
Block a user