UNPKG

@ckeditor/ckeditor5-table

Version:

Table feature for CKEditor 5.

338 lines (337 loc) • 13.1 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module table/tableui */ import { icons, Plugin } from 'ckeditor5/src/core.js'; import { addListToDropdown, createDropdown, ViewModel, SplitButtonView, SwitchButtonView, MenuBarMenuView } from 'ckeditor5/src/ui.js'; import { Collection } from 'ckeditor5/src/utils.js'; import InsertTableView from './ui/inserttableview.js'; import tableColumnIcon from './../theme/icons/table-column.svg'; import tableRowIcon from './../theme/icons/table-row.svg'; import tableMergeCellIcon from './../theme/icons/table-merge-cell.svg'; /** * The table UI plugin. It introduces: * * * The `'insertTable'` dropdown, * * The `'menuBar:insertTable'` menu bar menu, * * The `'tableColumn'` dropdown, * * The `'tableRow'` dropdown, * * The `'mergeTableCells'` split button. * * The `'tableColumn'`, `'tableRow'` and `'mergeTableCells'` dropdowns work best with {@link module:table/tabletoolbar~TableToolbar}. */ export default class TableUI extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'TableUI'; } /** * @inheritDoc */ static get isOfficialPlugin() { return true; } /** * @inheritDoc */ init() { const editor = this.editor; const t = this.editor.t; const contentLanguageDirection = editor.locale.contentLanguageDirection; const isContentLtr = contentLanguageDirection === 'ltr'; editor.ui.componentFactory.add('insertTable', locale => { const command = editor.commands.get('insertTable'); const dropdownView = createDropdown(locale); dropdownView.bind('isEnabled').to(command); // Decorate dropdown's button. dropdownView.buttonView.set({ icon: icons.table, label: t('Insert table'), tooltip: true }); let insertTableView; dropdownView.on('change:isOpen', () => { if (insertTableView) { return; } // Prepare custom view for dropdown's panel. insertTableView = new InsertTableView(locale); dropdownView.panelView.children.add(insertTableView); insertTableView.delegate('execute').to(dropdownView); dropdownView.on('execute', () => { editor.execute('insertTable', { rows: insertTableView.rows, columns: insertTableView.columns }); editor.editing.view.focus(); }); }); return dropdownView; }); editor.ui.componentFactory.add('menuBar:insertTable', locale => { const command = editor.commands.get('insertTable'); const menuView = new MenuBarMenuView(locale); const insertTableView = new InsertTableView(locale); insertTableView.delegate('execute').to(menuView); menuView.on('change:isOpen', (event, name, isOpen) => { if (!isOpen) { insertTableView.reset(); } }); insertTableView.on('execute', () => { editor.execute('insertTable', { rows: insertTableView.rows, columns: insertTableView.columns }); editor.editing.view.focus(); }); menuView.buttonView.set({ label: t('Table'), icon: icons.table }); menuView.panelView.children.add(insertTableView); menuView.bind('isEnabled').to(command); return menuView; }); editor.ui.componentFactory.add('tableColumn', locale => { const options = [ { type: 'switchbutton', model: { commandName: 'setTableColumnHeader', label: t('Header column'), bindIsOn: true } }, { type: 'separator' }, { type: 'button', model: { commandName: isContentLtr ? 'insertTableColumnLeft' : 'insertTableColumnRight', label: t('Insert column left') } }, { type: 'button', model: { commandName: isContentLtr ? 'insertTableColumnRight' : 'insertTableColumnLeft', label: t('Insert column right') } }, { type: 'button', model: { commandName: 'removeTableColumn', label: t('Delete column') } }, { type: 'button', model: { commandName: 'selectTableColumn', label: t('Select column') } } ]; return this._prepareDropdown(t('Column'), tableColumnIcon, options, locale); }); editor.ui.componentFactory.add('tableRow', locale => { const options = [ { type: 'switchbutton', model: { commandName: 'setTableRowHeader', label: t('Header row'), bindIsOn: true } }, { type: 'separator' }, { type: 'button', model: { commandName: 'insertTableRowAbove', label: t('Insert row above') } }, { type: 'button', model: { commandName: 'insertTableRowBelow', label: t('Insert row below') } }, { type: 'button', model: { commandName: 'removeTableRow', label: t('Delete row') } }, { type: 'button', model: { commandName: 'selectTableRow', label: t('Select row') } } ]; return this._prepareDropdown(t('Row'), tableRowIcon, options, locale); }); editor.ui.componentFactory.add('mergeTableCells', locale => { const options = [ { type: 'button', model: { commandName: 'mergeTableCellUp', label: t('Merge cell up') } }, { type: 'button', model: { commandName: isContentLtr ? 'mergeTableCellRight' : 'mergeTableCellLeft', label: t('Merge cell right') } }, { type: 'button', model: { commandName: 'mergeTableCellDown', label: t('Merge cell down') } }, { type: 'button', model: { commandName: isContentLtr ? 'mergeTableCellLeft' : 'mergeTableCellRight', label: t('Merge cell left') } }, { type: 'separator' }, { type: 'button', model: { commandName: 'splitTableCellVertically', label: t('Split cell vertically') } }, { type: 'button', model: { commandName: 'splitTableCellHorizontally', label: t('Split cell horizontally') } } ]; return this._prepareMergeSplitButtonDropdown(t('Merge cells'), tableMergeCellIcon, options, locale); }); } /** * Creates a dropdown view from a set of options. * * @param label The dropdown button label. * @param icon An icon for the dropdown button. * @param options The list of options for the dropdown. */ _prepareDropdown(label, icon, options, locale) { const editor = this.editor; const dropdownView = createDropdown(locale); const commands = this._fillDropdownWithListOptions(dropdownView, options); // Decorate dropdown's button. dropdownView.buttonView.set({ label, icon, tooltip: true }); // Make dropdown button disabled when all options are disabled. dropdownView.bind('isEnabled').toMany(commands, 'isEnabled', (...areEnabled) => { return areEnabled.some(isEnabled => isEnabled); }); this.listenTo(dropdownView, 'execute', evt => { editor.execute(evt.source.commandName); // Toggling a switch button view should not move the focus to the editable. if (!(evt.source instanceof SwitchButtonView)) { editor.editing.view.focus(); } }); return dropdownView; } /** * Creates a dropdown view with a {@link module:ui/dropdown/button/splitbuttonview~SplitButtonView} for * merge (and split)–related commands. * * @param label The dropdown button label. * @param icon An icon for the dropdown button. * @param options The list of options for the dropdown. */ _prepareMergeSplitButtonDropdown(label, icon, options, locale) { const editor = this.editor; const dropdownView = createDropdown(locale, SplitButtonView); const mergeCommandName = 'mergeTableCells'; // Main command. const mergeCommand = editor.commands.get(mergeCommandName); // Subcommands in the dropdown. const commands = this._fillDropdownWithListOptions(dropdownView, options); dropdownView.buttonView.set({ label, icon, tooltip: true, isEnabled: true }); // Make dropdown button disabled when all options are disabled together with the main command. dropdownView.bind('isEnabled').toMany([mergeCommand, ...commands], 'isEnabled', (...areEnabled) => { return areEnabled.some(isEnabled => isEnabled); }); // Merge selected table cells when the main part of the split button is clicked. this.listenTo(dropdownView.buttonView, 'execute', () => { editor.execute(mergeCommandName); editor.editing.view.focus(); }); // Execute commands for events coming from the list in the dropdown panel. this.listenTo(dropdownView, 'execute', evt => { editor.execute(evt.source.commandName); editor.editing.view.focus(); }); return dropdownView; } /** * Injects a {@link module:ui/list/listview~ListView} into the passed dropdown with buttons * which execute editor commands as configured in passed options. * * @param options The list of options for the dropdown. * @returns Commands the list options are interacting with. */ _fillDropdownWithListOptions(dropdownView, options) { const editor = this.editor; const commands = []; const itemDefinitions = new Collection(); for (const option of options) { addListOption(option, editor, commands, itemDefinitions); } addListToDropdown(dropdownView, itemDefinitions); return commands; } } /** * Adds an option to a list view. * * @param option A configuration option. * @param commands The list of commands to update. * @param itemDefinitions A collection of dropdown items to update with the given option. */ function addListOption(option, editor, commands, itemDefinitions) { if (option.type === 'button' || option.type === 'switchbutton') { const model = option.model = new ViewModel(option.model); const { commandName, bindIsOn } = option.model; const command = editor.commands.get(commandName); commands.push(command); model.set({ commandName }); model.bind('isEnabled').to(command); if (bindIsOn) { model.bind('isOn').to(command, 'value'); } model.set({ withText: true }); } itemDefinitions.add(option); }