@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
131 lines (124 loc) • 4.52 kB
JavaScript
/*
* Copyright (C) 2025 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import formatMessage from '../format-message';
// standard: string of tinymce menu commands
// e.g. 'instructure_links | inserttable instructure_media_embed | hr'
// custom: a string of tinymce menu commands
// returns: standard + custom with any duplicate commands removed from custom
export function mergeMenuItems(standard, custom) {
let c = custom?.trim?.();
if (!c) return standard;
const s = new Set(standard.split(/[\s|]+/));
// remove any duplicates
const c_array = c.split(/\s+/).filter(m => !s.has(m));
c = c_array.join(' ').replace(/^\s*\|\s*/, '').replace(/\s*\|\s*$/, '');
return `${standard} | ${c}`;
}
// standard: the incoming tinymce menu object
// custom: tinymce menu object to merge into standard
// returns: the merged result by mutating incoming standard arg.
// It will add commands to existing menus, or add a new menu
// if the custom one does not exist
export function mergeMenu(standard, custom) {
if (!custom) return standard;
Object.keys(custom).forEach(k => {
const curr_m = standard[k];
if (curr_m) {
curr_m.items = mergeMenuItems(curr_m.items, custom[k].items);
} else {
standard[k] = {
...custom[k]
};
}
});
return standard;
}
// standard: incoming tinymce toolbar array
// custom: tinymce toolbar array to merge into standard
// returns: the merged result by mutating the incoming standard arg.
// It will add commands to existing toolbars, or add a new toolbar
// if the custom one does not exist
export function mergeToolbar(standard, custom) {
if (!custom) return standard;
// merge given toolbar data into the default toolbar
custom.forEach(tb => {
const curr_tb = standard.find(t => tb.name && formatMessage(tb.name) === t.name);
if (curr_tb) {
curr_tb.items.splice(curr_tb.items.length, 0, ...tb.items);
} else {
standard.push(tb);
}
});
return standard;
}
// standard: incoming array of plugin names
// custom: array of plugin names to merge
// exclusions: array of plugins to remove
// returns: the merged result, duplicates and exclusions removed
export function mergePlugins(standard, custom = [], exclusions = []) {
const union = new Set(standard);
for (const c of custom) {
union.add(c);
}
for (const e of exclusions) {
union.delete(e);
}
return [...union];
}
export function focusToolbar(el) {
const $firstToolbarButton = el.querySelector('.tox-tbtn');
if ($firstToolbarButton) {
$firstToolbarButton.focus();
}
}
export function focusFirstMenuButton(el) {
const $firstMenu = el.querySelector('.tox-mbtn');
if ($firstMenu) {
$firstMenu.focus();
}
}
export function isElementWithinTable(node) {
let elem = node;
while (elem) {
if (elem.tagName === 'TABLE' || elem.tagName === 'TD' || elem.tagName === 'TH') {
return true;
}
elem = elem.parentElement;
}
return false;
}
// plugins is an array of strings
// the convention is that plugins starting with '-',
// i.e. a hyphen, are to be disabled in the RCE instance
export function parsePluginsToExclude(plugins) {
return plugins.filter(plugin => plugin.length > 0 && plugin[0] === '-').map(pluginToIgnore => pluginToIgnore.slice(1));
}
// if a placeholder image shows up in autosaved content, we have to remove it
// because the data url gets converted to a blob, which is not valid when restored.
// besides, the placeholder is intended to be temporary while the file
// is being uploaded
export function patchAutosavedContent(content, asText = false) {
const temp = document.createElement('div');
temp.innerHTML = content;
temp.querySelectorAll('[data-placeholder-for]').forEach(placeholder => {
// @ts-expect-error
placeholder.parentElement.removeChild(placeholder);
});
if (asText) return temp.textContent;
return temp.innerHTML;
}