@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
148 lines (146 loc) • 4.24 kB
JavaScript
/*
* Copyright (C) 2018 - 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 indicate from './indicate';
const ELEMENT_NODE = 1;
const WALK_BATCH_SIZE = 25;
const _indexOf = Array.prototype.indexOf;
export function walk(node, fn, done) {
const stack = [{
node,
index: 0
}];
const processBatch = () => {
let batchRemaining = WALK_BATCH_SIZE;
while (stack.length > 0 && batchRemaining > 0) {
const depth = stack.length - 1;
const node = stack[depth].node.childNodes[stack[depth].index];
if (node) {
stack[depth].index += 1;
if (node.nodeType === ELEMENT_NODE) {
fn(node);
stack.push({
node,
index: 0
});
batchRemaining -= 0;
}
} else {
stack.pop();
}
}
setTimeout(stack.length > 0 ? processBatch : done, 0);
};
processBatch();
}
export function select(elem, indicateFn = indicate) {
if (elem == null) {
return;
}
elem.scrollIntoView(false);
if (elem.ownerDocument?.documentElement) {
elem.ownerDocument.documentElement.scrollTop += 5; // room for the indicator highlight
}
indicateFn(elem);
}
export function prepend(parent, child) {
if (parent.childNodes.length > 0) {
parent.insertBefore(child, parent.childNodes[0]);
} else {
parent.appendChild(child);
}
}
export function changeTag(elem, tagName) {
const newElem = elem.ownerDocument.createElement(tagName);
while (elem.firstChild) {
newElem.appendChild(elem.firstChild);
}
for (let i = elem.attributes.length - 1; i >= 0; --i) {
newElem.attributes.setNamedItem(elem.attributes[i].cloneNode());
}
elem.parentNode.replaceChild(newElem, elem);
return newElem;
}
export function pathForNode(ancestor, decendant) {
const path = [];
let node = decendant;
while (true) {
if (node == ancestor) {
return path;
}
const parent = node.parentNode;
if (parent == null) {
return null;
}
path.push(_indexOf.call(parent.childNodes, node));
node = parent;
}
}
export function nodeByPath(ancestor, path) {
let node = ancestor;
let index;
while ((index = path.pop()) !== undefined) {
node = node.childNodes[index];
if (node == null) {
return null;
}
}
return node;
}
export function onlyContainsLink(elem) {
const links = elem.getElementsByTagName('a');
if (links.length) {
return links[0].textContent === elem.textContent;
} else {
return false;
}
}
export function splitStyleAttribute(styleString) {
const split = styleString.split(';');
return split.reduce((styleObj, attributeValue) => {
const pair = attributeValue.split(':');
if (pair.length === 2) {
styleObj[pair[0].trim()] = pair[1].trim();
}
return styleObj;
}, {});
}
export function createStyleString(styleObj) {
let styleString = Object.keys(styleObj).map(key => `${key}:${styleObj[key]}`).join(';');
if (styleString) {
styleString = `${styleString};`;
}
return styleString;
}
export function hasTextNode(elem) {
const nodes = Array.from(elem.childNodes);
return nodes.some(x => x.nodeType === Node.TEXT_NODE);
}
/**
* Notifies TinyMCE that a change has been made
* This ensures that changes persist in the editor's state without requiring additional user actions
* @returns {void}
*/
export function notifyTinyMCE() {
// Get the active TinyMCE editor instance
const editor = window.tinymce?.activeEditor;
if (editor) {
// Mark the content as dirty to ensure TinyMCE knows it has changed
editor.undoManager.add();
editor.fire('Change');
}
}