/** * Copyright (c) Tiny Technologies, Inc. All rights reserved. * Licensed under the LGPL or a commercial license. * For LGPL see License.txt in the project root for license information. * For commercial licenses see https://www.tiny.cloud/ */ import { AddEventsBehaviour, AlloyComponent, AlloyEvents, AlloyTriggers, Behaviour, EventFormat, FormField as AlloyFormField, Keying, NativeEvents, Replacing, Representing, SimulatedEvent, SketchSpec, SugarEvent, SystemEvents, Tabstopping, } from '@ephox/alloy'; import { Types } from '@ephox/bridge'; import { Arr, Fun } from '@ephox/katamari'; import { Attr, Element, Focus, Html, SelectorFind, Class } from '@ephox/sugar'; import { renderFormFieldWith, renderLabel } from 'tinymce/themes/silver/ui/alien/FieldLabeller'; import { detectSize } from '../alien/FlatgridAutodetect'; import { formActionEvent, formResizeEvent } from '../general/FormEvents'; import { deriveCollectionMovement } from '../menus/menu/MenuMovement'; import * as ItemClasses from '../menus/item/ItemClasses'; import { UiFactoryBackstageProviders } from '../../backstage/Backstage'; import { Omit } from '../Omit'; import I18n from 'tinymce/core/api/util/I18n'; import { HTMLElement } from '@ephox/dom-globals'; type CollectionSpec = Omit; export const renderCollection = (spec: CollectionSpec, providersBackstage: UiFactoryBackstageProviders): SketchSpec => { // DUPE with TextField. const pLabel = spec.label.map((label) => renderLabel(label, providersBackstage)); const runOnItem = (f: (c: AlloyComponent, tgt: Element, itemValue: string) => void) => (comp: AlloyComponent, se: SimulatedEvent) => { SelectorFind.closest(se.event().target(), '[data-collection-item-value]').each((target: Element) => { f(comp, target, Attr.get(target, 'data-collection-item-value')); }); }; const escapeAttribute = (ch) => { if (ch === '"') { return '"'; } return ch; }; const setContents = (comp, items) => { const htmlLines = Arr.map(items, (item) => { const itemText = I18n.translate(item.text); const textContent = spec.columns === 1 ? `
${itemText}
` : ''; const iconContent = `
${item.icon}
`; // Replacing the hyphens and underscores in collection items with spaces // to ensure the screen readers pronounce the words correctly. // This is only for aria purposes. Emoticon and Special Character names will still use _ and - for autocompletion. const mapItemName = { '_': ' ', ' - ': ' ', '-': ' ' }; // Title attribute is added here to provide tooltips which might be helpful to sighted users. // Using aria-label here overrides the Apple description of emojis and special characters in Mac/ MS description in Windows. // But if only the title attribute is used instead, the names are read out twice. i.e., the description followed by the item.text. const ariaLabel = itemText.replace(/\_| \- |\-/g, (match) => { return mapItemName[match]; }); return `
${iconContent}${textContent}
`; }); const chunks = spec.columns > 1 && spec.columns !== 'auto' ? Arr.chunk(htmlLines, spec.columns) : [ htmlLines ]; const html = Arr.map(chunks, (ch) => { return `
${ch.join('')}
`; }); Html.set(comp.element(), html.join('')); }; const collectionEvents = [ AlloyEvents.run(NativeEvents.mouseover(), runOnItem((comp, tgt) => { Focus.focus(tgt); })), AlloyEvents.run(SystemEvents.tapOrClick(), runOnItem((comp, tgt, itemValue) => { AlloyTriggers.emitWith(comp, formActionEvent, { name: spec.name, value: itemValue }); })), AlloyEvents.run(NativeEvents.focusin(), runOnItem((comp, tgt, itemValue) => { SelectorFind.descendant(comp.element(), '.' + ItemClasses.activeClass).each((currentActive) => { Class.remove(currentActive, ItemClasses.activeClass); }); Class.add(tgt, ItemClasses.activeClass); })), AlloyEvents.run(NativeEvents.focusout(), runOnItem((comp, tgt, itemValue) => { SelectorFind.descendant(comp.element(), '.' + ItemClasses.activeClass).each((currentActive) => { Class.remove(currentActive, ItemClasses.activeClass); }); })), AlloyEvents.runOnExecute(runOnItem((comp, tgt, itemValue) => { AlloyTriggers.emitWith(comp, formActionEvent, { name: spec.name, value: itemValue }); })) ]; const pField = AlloyFormField.parts().field({ dom: { tag: 'div', // FIX: Read from columns classes: [ 'tox-collection' ].concat(spec.columns !== 1 ? [ 'tox-collection--grid' ] : [ 'tox-collection--list' ]) }, components: [ ], factory: { sketch: Fun.identity }, behaviours: Behaviour.derive([ Replacing.config({ }), Representing.config({ store: { mode: 'memory', initialValue: [ ] }, onSetValue: (comp, items) => { setContents(comp, items); if (spec.columns === 'auto') { detectSize(comp, 5, 'tox-collection__item').each(({ numRows, numColumns }) => { Keying.setGridSize(comp, numRows, numColumns); }); } AlloyTriggers.emit(comp, formResizeEvent); } }), Tabstopping.config({ }), Keying.config( deriveCollectionMovement(spec.columns, 'normal') ), AddEventsBehaviour.config('collection-events', collectionEvents) ]) }); const extraClasses = ['tox-form__group--collection']; return renderFormFieldWith(pLabel, pField, extraClasses, [ ]); };