mirror of
https://github.com/tinymce/tinymce.git
synced 2025-07-29 21:23:59 +00:00
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:
6
.changes/unreleased/tinymce-TINY-11095-2024-10-10.yaml
Normal file
6
.changes/unreleased/tinymce-TINY-11095-2024-10-10.yaml
Normal 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
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -124,6 +124,7 @@
|
||||
}
|
||||
|
||||
.tox-tbtn,
|
||||
.tox-label,
|
||||
.tox-number-input,
|
||||
.tox-tbtn--select,
|
||||
.tox-split-button {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
30
modules/tinymce/src/core/demo/html/context_toolbar_demo.html
Normal file
30
modules/tinymce/src/core/demo/html/context_toolbar_demo.html
Normal 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>
|
50
modules/tinymce/src/core/demo/ts/demo/ContextToolbarDemo.ts
Normal file
50
modules/tinymce/src/core/demo/ts/demo/ContextToolbarDemo.ts
Normal 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' }));
|
||||
};
|
@ -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,
|
||||
|
@ -38,6 +38,7 @@ export type URLConverterCallback = (url: string, node: Node | string | undefined
|
||||
|
||||
export interface ToolbarGroup {
|
||||
name?: string;
|
||||
label?: string;
|
||||
items: string[];
|
||||
}
|
||||
|
||||
|
@ -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[]
|
||||
}
|
||||
];
|
||||
|
@ -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({
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user