TINY-11095: add inline label for toolbars' groups (#9900)

* experimenting with toolbar groups

* TINY-11095: using of `fieldset` for groups with label

* TINY-11095: first implementation of the styles

* TINY-11095: clean css file

* TINY-11095: css variables and adjust to designs

* TINY-11095: fix

* TINY-11095: fix

* TINY-11095: fix

* TINY-11095: refactor

* TINY-11095: fix schema in context toolbar

* TINY-11095: revert `fieldset` to `div`

* TINY-11095: add demo

* TINY-11095: revert FullDemo

* TINY-11095: fix

* TINY-11095: export `ToolbarGroup`

* TINY-11095: add changelog entry

* TINY-11095: fix changelog entry

* TINY-11095: fix css

* TINY-11095: fix class name

* TINY-11095: remove unused CSS

* TINY-11095: remove comment

---------

Co-authored-by: Spocke <spocke@moxiecode.com>
Co-authored-by: Zuzanna Tomaszyk <zuzanna.tomaszyk@tiny.cloud>
This commit is contained in:
lorenzo-pomili
2024-10-14 09:17:47 +02:00
committed by Spocke
parent 4c2ca1612d
commit 2d061a54db
15 changed files with 186 additions and 22 deletions

View File

@ -0,0 +1,6 @@
project: tinymce
kind: Added
body: It is now possible to create labeled groups in context toolbars.
time: 2024-10-10T09:48:36.710841346+02:00
custom:
Issue: TINY-11095

View File

@ -7,7 +7,7 @@ import {
ContextForm, ContextFormButton, ContextFormButtonInstanceApi, ContextFormButtonSpec, ContextFormInstanceApi, ContextFormSpec,
ContextFormToggleButton, ContextFormToggleButtonInstanceApi, ContextFormToggleButtonSpec, createContextForm
} from '../components/content/ContextForm';
import { ContextToolbar, ContextToolbarSpec, createContextToolbar } from '../components/content/ContextToolbar';
import { ContextToolbar, ContextToolbarSpec, createContextToolbar, contextToolbarToSpec, ToolbarGroup } from '../components/content/ContextToolbar';
export {
AutocompleterSpec,
@ -37,6 +37,8 @@ export {
ContextToolbar,
ContextToolbarSpec,
createContextToolbar,
contextToolbarToSpec,
ToolbarGroup,
SeparatorItemSpec,
SeparatorItem,

View File

@ -1,23 +1,53 @@
import { FieldSchema, StructureSchema } from '@ephox/boulder';
import { Result } from '@ephox/katamari';
import { FieldSchema, StructureSchema, ValueType } from '@ephox/boulder';
import { Arr, Optional, Result, Type } from '@ephox/katamari';
import * as ComponentSchema from '../../core/ComponentSchema';
import { ContextBar, contextBarFields, ContextBarSpec } from './ContextBar';
export interface ToolbarGroupSpec {
name?: string;
label?: string;
items: string[];
}
export interface ContextToolbarSpec extends ContextBarSpec {
type?: 'contexttoolbar';
items: string;
items: string | ToolbarGroupSpec[];
}
export interface ToolbarGroup {
name: Optional<string>;
label: Optional<string>;
items: string[];
}
export interface ContextToolbar extends ContextBar {
type: 'contexttoolbar';
items: string;
items: string | ToolbarGroup[];
}
const contextToolbarSchema = StructureSchema.objOf([
ComponentSchema.defaultedType('contexttoolbar'),
FieldSchema.requiredString('items')
FieldSchema.requiredOf('items', StructureSchema.oneOf([
ValueType.string,
StructureSchema.arrOfObj([
FieldSchema.optionString('name'),
FieldSchema.optionString('label'),
FieldSchema.requiredArrayOf('items', ValueType.string)
])
])),
].concat(contextBarFields));
const toolbarGroupBackToSpec = (toolbarGroup: ToolbarGroup): ToolbarGroupSpec => ({
name: toolbarGroup.name.getOrUndefined(),
label: toolbarGroup.label.getOrUndefined(),
items: toolbarGroup.items
});
export const contextToolbarToSpec = (contextToolbar: ContextToolbar): ContextToolbarSpec => ({
...contextToolbar,
items: Type.isString(contextToolbar.items) ? contextToolbar.items : Arr.map(contextToolbar.items, toolbarGroupBackToSpec)
});
export const createContextToolbar = (spec: ContextToolbarSpec): Result<ContextToolbar, StructureSchema.SchemaError<any>> =>
StructureSchema.asRaw<ContextToolbar>('ContextToolbar', contextToolbarSchema, spec);

View File

@ -124,6 +124,7 @@
}
.tox-tbtn,
.tox-label,
.tox-number-input,
.tox-tbtn--select,
.tox-split-button {

View File

@ -0,0 +1,15 @@
//
// Toolbar label
//
@toolbar-label-spacing-x: 1px;
@toolbar-label-spacing-y: 5px;
.tox {
.tox-label--context-toolbar {
margin: (@toolbar-label-spacing-y + 1px) @toolbar-label-spacing-x @toolbar-label-spacing-y 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
}

View File

@ -61,6 +61,7 @@
@import 'components/statusbar/statusbar';
@import 'components/throbber/throbber';
@import 'components/toolbar-button/toolbar-button';
@import 'components/toolbar-button/toolbar-label';
@import 'components/toolbar-button/toolbar-number-input';
@import 'components/toolbar-button/toolbar-select-button';
@import 'components/toolbar-button/toolbar-split-button';

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>TinyMCE Context Toolbar Demo Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Context-Toolbar"
content="default-src 'none'; script-src 'self'; connect-src 'self' blob:; img-src 'self' data: blob:; style-src 'self'; font-src 'self';" />
</head>
<body>
<main>
<h1>TinyMCE Context Toolbar Demo Page</h1>
<div id="ephox-ui">
<textarea>
<p>some P</p>
<div>some Div</div>
</textarea>
</div>
<div class="tinymce">Inline demo
<p>some P</p>
<div>some Div</div>
</div>
<script src="../../../../js/tinymce/tinymce.js"></script>
<script src="../../../../scratch/demos/core/demo.js"></script>
<script>demos.ContextToolbarDemo();</script>
</main>
</body>
</html>

View File

@ -0,0 +1,50 @@
import { Merger } from '@ephox/katamari';
import { RawEditorOptions, TinyMCE } from 'tinymce/core/api/PublicApi';
declare let tinymce: TinyMCE;
export default (): void => {
const settings: RawEditorOptions = {
skin_url: '../../../../js/tinymce/skins/ui/oxide',
selector: 'textarea',
plugins: [
'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample'
],
toolbar: 'bold italic',
setup: (ed) => {
ed.ui.registry.addContextToolbar('foo', {
predicate: (node) => node.nodeName.toUpperCase() === 'P',
items: [
{
// label: 'Formatting',
name: 'Formatting',
items: [ 'bold', 'italic' ]
},
{
label: 'History',
items: [ 'undo', 'redo' ]
},
{
items: [ 'undo', 'italic' ]
}
],
position: 'line',
scope: 'editor'
});
ed.ui.registry.addContextToolbar('bar', {
predicate: (node) => node.nodeName.toUpperCase() === 'DIV',
items: 'bold italic | undo redo',
position: 'line',
scope: 'editor'
});
}
};
tinymce.init(settings);
tinymce.init(Merger.deepMerge(settings, { inline: true, selector: 'div.tinymce' }));
};

View File

@ -1,6 +1,7 @@
import AnnotationsDemo from './AnnotationsDemo';
import CommandsDemo from './CommandsDemo';
import ContentEditableFalseDemo from './ContentEditableFalseDemo';
import ContextToolbarDemo from './ContextToolbarDemo';
import CustomThemeDemo from './CustomThemeDemo';
import FixedToolbarContainerDemo from './FixedToolbarContainerDemo';
import FullDemo from './FullDemo';
@ -20,6 +21,7 @@ declare const window: any;
window.demos = {
CommandsDemo,
ContentEditableFalseDemo,
ContextToolbarDemo,
CustomThemeDemo,
IframeDemo,
InlineDemo,

View File

@ -38,6 +38,7 @@ export type URLConverterCallback = (url: string, node: Node | string | undefined
export interface ToolbarGroup {
name?: string;
label?: string;
items: string[];
}

View File

@ -53,10 +53,12 @@ const buildInitGroups = (ctx: InlineContent.ContextForm, providers: UiFactoryBac
return [
{
title: Optional.none(),
label: Optional.none(),
items: [ memInput.asSpec() ]
},
{
title: Optional.none(),
label: Optional.none(),
items: commands.asSpecs() as AlloySpec[]
}
];

View File

@ -141,8 +141,9 @@ const register = (editor: Editor, registryContextToolbars: Record<string, Contex
});
}));
const buildContextToolbarGroups = (allButtons: Record<string, ContextToolbarButtonType>, ctx: InlineContent.ContextToolbarSpec) =>
identifyButtons(editor, { buttons: allButtons, toolbar: ctx.items, allowToolbarGroups: false }, extras.backstage, Optional.some([ 'form:' ]));
const buildContextToolbarGroups = (allButtons: Record<string, ContextToolbarButtonType>, ctx: InlineContent.ContextToolbarSpec) => {
return identifyButtons(editor, { buttons: allButtons, toolbar: ctx.items, allowToolbarGroups: false }, extras.backstage, Optional.some([ 'form:' ]));
};
const buildContextFormGroups = (ctx: InlineContent.ContextForm, providers: UiFactoryBackstageProviders) => ContextForm.buildInitGroups(ctx, providers);
@ -156,7 +157,7 @@ const register = (editor: Editor, registryContextToolbars: Record<string, Contex
const toolbarType = getToolbarMode(editor) === ToolbarMode.scrolling ? ToolbarMode.scrolling : ToolbarMode.default;
const initGroups = Arr.flatten(Arr.map(toolbars, (ctx) =>
ctx.type === 'contexttoolbar' ? buildContextToolbarGroups(allButtons, ctx) : buildContextFormGroups(ctx, sharedBackstage.providers)
ctx.type === 'contexttoolbar' ? buildContextToolbarGroups(allButtons, InlineContent.contextToolbarToSpec(ctx)) : buildContextFormGroups(ctx, sharedBackstage.providers)
));
return renderToolbar({

View File

@ -1,8 +1,13 @@
// eslint-disable-next-line max-len
import {
AddEventsBehaviour, AlloyComponent, AlloyEvents, AlloySpec, Behaviour, Boxes, Focusing, Keying, SketchSpec,
AddEventsBehaviour, AlloyComponent, AlloyEvents, AlloySpec,
SplitFloatingToolbar as AlloySplitFloatingToolbar,
SplitSlidingToolbar as AlloySplitSlidingToolbar, Tabstopping, Toolbar as AlloyToolbar, ToolbarGroup as AlloyToolbarGroup
SplitSlidingToolbar as AlloySplitSlidingToolbar,
Toolbar as AlloyToolbar, ToolbarGroup as AlloyToolbarGroup,
Behaviour, Boxes, Focusing,
GuiFactory,
Keying, SketchSpec,
Tabstopping
} from '@ephox/alloy';
import { Arr, Optional, Result } from '@ephox/katamari';
import { Traverse } from '@ephox/sugar';
@ -39,20 +44,35 @@ export interface MoreDrawerToolbarSpec extends ToolbarSpec {
export interface ToolbarGroup {
readonly title: Optional<string>;
readonly label: Optional<string>;
readonly items: AlloySpec[];
}
const renderToolbarGroupCommon = (toolbarGroup: ToolbarGroup) => {
const attributes = toolbarGroup.title.fold(() => ({}),
(title) => ({ attributes: { title }}));
const attributes = toolbarGroup.label.isNone() ? toolbarGroup.title.fold(() => ({}),
(title) => ({ attributes: { title }})) : toolbarGroup.label.fold(() => ({}),
(label) => ({ attributes: { 'aria-label': label }})
);
return {
dom: {
tag: 'div',
classes: [ 'tox-toolbar__group' ],
classes: [ 'tox-toolbar__group' ].concat(
toolbarGroup.label.isSome() ? [ 'tox-toolbar__group_with_label' ] : []
),
...attributes
},
components: [
...(toolbarGroup.label.map((label) => {
return {
dom: {
tag: 'span',
classes: [ 'tox-label', 'tox-label--context-toolbar' ],
},
components: [ GuiFactory.text(label) ]
};
}).toArray()),
AlloyToolbarGroup.parts.items({})
],
@ -106,6 +126,7 @@ const renderMoreToolbarCommon = (toolbarSpec: MoreDrawerToolbarSpec) => {
// This already knows it is a toolbar group
'overflow-group': renderToolbarGroupCommon({
title: Optional.none(),
label: Optional.none(),
items: []
}),
'overflow-button': renderIconButtonSpec({
@ -229,4 +250,5 @@ const renderToolbar = (toolbarSpec: ToolbarSpec): SketchSpec => {
});
};
export { renderToolbarGroup, renderToolbar, renderFloatingMoreToolbar, renderSlidingMoreToolbar };
export { renderFloatingMoreToolbar, renderSlidingMoreToolbar, renderToolbar, renderToolbarGroup };

View File

@ -1,7 +1,7 @@
import { AlloySpec, VerticalDir } from '@ephox/alloy';
import { StructureSchema } from '@ephox/boulder';
import { Toolbar } from '@ephox/bridge';
import { Arr, Obj, Optional, Result, Type } from '@ephox/katamari';
import { Arr, Obj, Optional, Optionals, Result, Type } from '@ephox/katamari';
import Editor from 'tinymce/core/api/Editor';
@ -143,7 +143,7 @@ const convertStringToolbar = (strToolbar: string) => {
};
const isToolbarGroupSettingArray = (toolbar: ToolbarConfig): toolbar is ToolbarGroupOption[] =>
Type.isArrayOf(toolbar, (t): t is ToolbarGroupOption => Obj.has(t, 'name') && Obj.has(t, 'items'));
Type.isArrayOf(toolbar, (t): t is ToolbarGroupOption => (Obj.has(t, 'name') || Obj.has(t, 'label')) && Obj.has(t, 'items'));
// Toolbar settings
// false = disabled
@ -200,6 +200,7 @@ const identifyButtons = (editor: Editor, toolbarConfig: RenderToolbarConfig, bac
});
return {
title: Optional.from(editor.translate(group.name)),
label: Optionals.someIf(group.label !== undefined, editor.translate(group.label)),
items
};
});

View File

@ -33,13 +33,13 @@ describe('headless.tinymce.themes.silver.toolbar.ToolbarTest', () => {
providers,
initGroups: [
{
title: Optional.none(), items: Arr.map([ 'one', 'two', 'three' ], makeButton)
title: Optional.none(), label: Optional.none(), items: Arr.map([ 'one', 'two', 'three' ], makeButton)
},
{
title: Optional.some('group title'), items: Arr.map([ 'four', 'five' ], makeButton)
title: Optional.some('group title'), label: Optional.none(), items: Arr.map([ 'four', 'five' ], makeButton)
},
{
title: Optional.some('another group title'), items: Arr.map([ 'six' ], makeButton)
title: Optional.some('another group title'), label: Optional.none(), items: Arr.map([ 'six' ], makeButton)
}
]
})
@ -121,10 +121,10 @@ describe('headless.tinymce.themes.silver.toolbar.ToolbarTest', () => {
const doc = SugarDocument.getDocument();
const groups = Arr.map([
{
title: Optional.none<string>(), items: Arr.map([ 'A', 'B' ], makeButton)
title: Optional.none<string>(), label: Optional.none(), items: Arr.map([ 'A', 'B' ], makeButton)
},
{
title: Optional.none<string>(), items: Arr.map([ 'C' ], makeButton)
title: Optional.none<string>(), label: Optional.none(), items: Arr.map([ 'C' ], makeButton)
}
], renderToolbarGroup);