This commit is contained in:
2025-10-22 15:39:40 +08:00
commit b0b510fac1
2720 changed files with 415933 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Lists Demo Page</title>
</head>
<body>
<h2>Lists Demo Page</h2>
<div id="ephox-ui">
<textarea name="" id="" cols="30" rows="10" class="tinymce"></textarea>
</div>
<script src="../../../../../js/tinymce/tinymce.js"></script>
<script src="../../../../../scratch/demos/plugins/lists/demo.js"></script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
/**
* Demo.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
declare let tinymce: any;
tinymce.init({
selector: 'textarea.tinymce',
theme: 'modern',
skin_url: '../../../../../js/tinymce/skins/lightgray',
plugins: 'lists code',
toolbar: 'numlist bullist | outdent indent | code',
height: 600
});
export {};

View File

@@ -0,0 +1,22 @@
/**
* 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 PluginManager from 'tinymce/core/api/PluginManager';
import Api from './api/Api';
import Commands from './api/Commands';
import Keyboard from './core/Keyboard';
import Buttons from './ui/Buttons';
PluginManager.add('lists', function (editor) {
Keyboard.setup(editor);
Buttons.register(editor);
Commands.register(editor);
return Api.get(editor);
});
export default function () { }

View File

@@ -0,0 +1,53 @@
/**
* 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 { Arr } from '@ephox/katamari';
import { Element} from '@ephox/sugar';
import { Editor } from 'tinymce/core/api/Editor';
import { Indentation } from '../listModel/Indentation';
import { listsIndentation } from '../listModel/ListsIndendation';
import { dlIndentation } from '../core/DlIndentation';
import Range from '../core/Range';
import Selection from '../core/Selection';
const selectionIndentation = (editor: Editor, indentation: Indentation): boolean => {
const lists = Arr.map(Selection.getSelectedListRoots(editor), Element.fromDom);
const dlItems = Arr.map(Selection.getSelectedDlItems(editor), Element.fromDom);
let isHandled = false;
if (lists.length || dlItems.length) {
const bookmark = editor.selection.getBookmark();
listsIndentation(editor, lists, indentation);
dlIndentation(editor, indentation, dlItems);
editor.selection.moveToBookmark(bookmark);
editor.selection.setRng(Range.normalizeRange(editor.selection.getRng()));
editor.nodeChanged();
isHandled = true;
}
return isHandled;
};
const indentListSelection = (editor: Editor): boolean => {
return selectionIndentation(editor, Indentation.Indent);
};
const outdentListSelection = (editor: Editor): boolean => {
return selectionIndentation(editor, Indentation.Outdent);
};
const flattenListSelection = (editor: Editor): boolean => {
return selectionIndentation(editor, Indentation.Flatten);
};
export {
indentListSelection,
outdentListSelection,
flattenListSelection
};

View File

@@ -0,0 +1,282 @@
/**
* 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 BookmarkManager from 'tinymce/core/api/dom/BookmarkManager';
import Tools from 'tinymce/core/api/util/Tools';
import Bookmark from '../core/Bookmark';
import NodeType from '../core/NodeType';
import Selection from '../core/Selection';
import { HTMLElement } from '@ephox/dom-globals';
import { flattenListSelection } from './Indendation';
const updateListStyle = function (dom, el, detail) {
const type = detail['list-style-type'] ? detail['list-style-type'] : null;
dom.setStyle(el, 'list-style-type', type);
};
const setAttribs = function (elm, attrs) {
Tools.each(attrs, function (value, key) {
elm.setAttribute(key, value);
});
};
const updateListAttrs = function (dom, el, detail) {
setAttribs(el, detail['list-attributes']);
Tools.each(dom.select('li', el), function (li) {
setAttribs(li, detail['list-item-attributes']);
});
};
const updateListWithDetails = function (dom, el, detail) {
updateListStyle(dom, el, detail);
updateListAttrs(dom, el, detail);
};
const removeStyles = (dom, element: HTMLElement, styles: string[]) => {
Tools.each(styles, (style) => dom.setStyle(element, { [style]: '' }));
};
const getEndPointNode = function (editor, rng, start, root) {
let container, offset;
container = rng[start ? 'startContainer' : 'endContainer'];
offset = rng[start ? 'startOffset' : 'endOffset'];
// Resolve node index
if (container.nodeType === 1) {
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
}
if (!start && NodeType.isBr(container.nextSibling)) {
container = container.nextSibling;
}
while (container.parentNode !== root) {
if (NodeType.isTextBlock(editor, container)) {
return container;
}
if (/^(TD|TH)$/.test(container.parentNode.nodeName)) {
return container;
}
container = container.parentNode;
}
return container;
};
const getSelectedTextBlocks = function (editor, rng, root) {
const textBlocks = [], dom = editor.dom;
const startNode = getEndPointNode(editor, rng, true, root);
const endNode = getEndPointNode(editor, rng, false, root);
let block;
const siblings = [];
for (let node = startNode; node; node = node.nextSibling) {
siblings.push(node);
if (node === endNode) {
break;
}
}
Tools.each(siblings, function (node) {
if (NodeType.isTextBlock(editor, node)) {
textBlocks.push(node);
block = null;
return;
}
if (dom.isBlock(node) || NodeType.isBr(node)) {
if (NodeType.isBr(node)) {
dom.remove(node);
}
block = null;
return;
}
const nextSibling = node.nextSibling;
if (BookmarkManager.isBookmarkNode(node)) {
if (NodeType.isTextBlock(editor, nextSibling) || (!nextSibling && node.parentNode === root)) {
block = null;
return;
}
}
if (!block) {
block = dom.create('p');
node.parentNode.insertBefore(block, node);
textBlocks.push(block);
}
block.appendChild(node);
});
return textBlocks;
};
const hasCompatibleStyle = function (dom, sib, detail) {
const sibStyle = dom.getStyle(sib, 'list-style-type');
let detailStyle = detail ? detail['list-style-type'] : '';
detailStyle = detailStyle === null ? '' : detailStyle;
return sibStyle === detailStyle;
};
const applyList = function (editor, listName: string, detail = {}) {
const rng = editor.selection.getRng(true);
let bookmark;
let listItemName = 'LI';
const root = Selection.getClosestListRootElm(editor, editor.selection.getStart(true));
const dom = editor.dom;
if (dom.getContentEditable(editor.selection.getNode()) === 'false') {
return;
}
listName = listName.toUpperCase();
if (listName === 'DL') {
listItemName = 'DT';
}
bookmark = Bookmark.createBookmark(rng);
Tools.each(getSelectedTextBlocks(editor, rng, root), function (block) {
let listBlock, sibling;
sibling = block.previousSibling;
if (sibling && NodeType.isListNode(sibling) && sibling.nodeName === listName && hasCompatibleStyle(dom, sibling, detail)) {
listBlock = sibling;
block = dom.rename(block, listItemName);
sibling.appendChild(block);
} else {
listBlock = dom.create(listName);
block.parentNode.insertBefore(listBlock, block);
listBlock.appendChild(block);
block = dom.rename(block, listItemName);
}
removeStyles(dom, block, [
'margin', 'margin-right', 'margin-bottom', 'margin-left', 'margin-top',
'padding', 'padding-right', 'padding-bottom', 'padding-left', 'padding-top',
]);
updateListWithDetails(dom, listBlock, detail);
mergeWithAdjacentLists(editor.dom, listBlock);
});
editor.selection.setRng(Bookmark.resolveBookmark(bookmark));
};
const isValidLists = function (list1, list2) {
return list1 && list2 && NodeType.isListNode(list1) && list1.nodeName === list2.nodeName;
};
const hasSameListStyle = function (dom, list1, list2) {
const targetStyle = dom.getStyle(list1, 'list-style-type', true);
const style = dom.getStyle(list2, 'list-style-type', true);
return targetStyle === style;
};
const hasSameClasses = function (elm1, elm2) {
return elm1.className === elm2.className;
};
const shouldMerge = function (dom, list1, list2) {
return isValidLists(list1, list2) && hasSameListStyle(dom, list1, list2) && hasSameClasses(list1, list2);
};
const mergeWithAdjacentLists = function (dom, listBlock) {
let sibling, node;
sibling = listBlock.nextSibling;
if (shouldMerge(dom, listBlock, sibling)) {
while ((node = sibling.firstChild)) {
listBlock.appendChild(node);
}
dom.remove(sibling);
}
sibling = listBlock.previousSibling;
if (shouldMerge(dom, listBlock, sibling)) {
while ((node = sibling.lastChild)) {
listBlock.insertBefore(node, listBlock.firstChild);
}
dom.remove(sibling);
}
};
const updateList = function (dom, list, listName, detail) {
if (list.nodeName !== listName) {
const newList = dom.rename(list, listName);
updateListWithDetails(dom, newList, detail);
} else {
updateListWithDetails(dom, list, detail);
}
};
const toggleMultipleLists = function (editor, parentList, lists, listName, detail) {
if (parentList.nodeName === listName && !hasListStyleDetail(detail)) {
flattenListSelection(editor);
} else {
const bookmark = Bookmark.createBookmark(editor.selection.getRng(true));
Tools.each([parentList].concat(lists), function (elm) {
updateList(editor.dom, elm, listName, detail);
});
editor.selection.setRng(Bookmark.resolveBookmark(bookmark));
}
};
const hasListStyleDetail = function (detail) {
return 'list-style-type' in detail;
};
const toggleSingleList = function (editor, parentList, listName, detail) {
if (parentList === editor.getBody()) {
return;
}
if (parentList) {
if (parentList.nodeName === listName && !hasListStyleDetail(detail)) {
flattenListSelection(editor);
} else {
const bookmark = Bookmark.createBookmark(editor.selection.getRng(true));
updateListWithDetails(editor.dom, parentList, detail);
mergeWithAdjacentLists(editor.dom, editor.dom.rename(parentList, listName));
editor.selection.setRng(Bookmark.resolveBookmark(bookmark));
}
} else {
applyList(editor, listName, detail);
}
};
const toggleList = function (editor, listName, detail) {
const parentList = Selection.getParentList(editor);
const selectedSubLists = Selection.getSelectedSubLists(editor);
detail = detail ? detail : {};
if (parentList && selectedSubLists.length > 0) {
toggleMultipleLists(editor, parentList, selectedSubLists, listName, detail);
} else {
toggleSingleList(editor, parentList, listName, detail);
}
};
export default {
toggleList,
mergeWithAdjacentLists
};

View File

@@ -0,0 +1,20 @@
/**
* 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 Delete from '../core/Delete';
const get = function (editor) {
return {
backspaceDelete (isForward) {
Delete.backspaceDelete(editor, isForward);
}
};
};
export default {
get
};

View File

@@ -0,0 +1,52 @@
/**
* 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 ToggleList from '../actions/ToggleList';
import { indentListSelection, outdentListSelection, flattenListSelection } from '../actions/Indendation';
const queryListCommandState = function (editor, listName) {
return function () {
const parentList = editor.dom.getParent(editor.selection.getStart(), 'UL,OL,DL');
return parentList && parentList.nodeName === listName;
};
};
const register = function (editor) {
editor.on('BeforeExecCommand', function (e) {
const cmd = e.command.toLowerCase();
if (cmd === 'indent') {
indentListSelection(editor);
} else if (cmd === 'outdent') {
outdentListSelection(editor);
}
});
editor.addCommand('InsertUnorderedList', function (ui, detail) {
ToggleList.toggleList(editor, 'UL', detail);
});
editor.addCommand('InsertOrderedList', function (ui, detail) {
ToggleList.toggleList(editor, 'OL', detail);
});
editor.addCommand('InsertDefinitionList', function (ui, detail) {
ToggleList.toggleList(editor, 'DL', detail);
});
editor.addCommand('RemoveList', () => {
flattenListSelection(editor);
});
editor.addQueryStateHandler('InsertUnorderedList', queryListCommandState(editor, 'UL'));
editor.addQueryStateHandler('InsertOrderedList', queryListCommandState(editor, 'OL'));
editor.addQueryStateHandler('InsertDefinitionList', queryListCommandState(editor, 'DL'));
};
export default {
register
};

View File

@@ -0,0 +1,14 @@
/**
* 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/
*/
const shouldIndentOnTab = function (editor) {
return editor.getParam('lists_indent_on_tab', true);
};
export default {
shouldIndentOnTab
};

View File

@@ -0,0 +1,126 @@
/**
* 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 DOMUtils from 'tinymce/core/api/dom/DOMUtils';
import Range from './Range';
const DOM = DOMUtils.DOM;
/**
* Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
* index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
* added to them since they can be restored after a dom operation.
*
* So this: <p><b>|</b><b>|</b></p>
* becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p>
*
* @param {DOMRange} rng DOM Range to get bookmark on.
* @return {Object} Bookmark object.
*/
const createBookmark = function (rng) {
const bookmark = {};
const setupEndPoint = function (start?) {
let offsetNode, container, offset;
container = rng[start ? 'startContainer' : 'endContainer'];
offset = rng[start ? 'startOffset' : 'endOffset'];
if (container.nodeType === 1) {
offsetNode = DOM.create('span', { 'data-mce-type': 'bookmark' });
if (container.hasChildNodes()) {
offset = Math.min(offset, container.childNodes.length - 1);
if (start) {
container.insertBefore(offsetNode, container.childNodes[offset]);
} else {
DOM.insertAfter(offsetNode, container.childNodes[offset]);
}
} else {
container.appendChild(offsetNode);
}
container = offsetNode;
offset = 0;
}
bookmark[start ? 'startContainer' : 'endContainer'] = container;
bookmark[start ? 'startOffset' : 'endOffset'] = offset;
};
setupEndPoint(true);
if (!rng.collapsed) {
setupEndPoint();
}
return bookmark;
};
const resolveBookmark = function (bookmark) {
function restoreEndPoint(start?) {
let container, offset, node;
const nodeIndex = function (container) {
let node = container.parentNode.firstChild, idx = 0;
while (node) {
if (node === container) {
return idx;
}
// Skip data-mce-type=bookmark nodes
if (node.nodeType !== 1 || node.getAttribute('data-mce-type') !== 'bookmark') {
idx++;
}
node = node.nextSibling;
}
return -1;
};
container = node = bookmark[start ? 'startContainer' : 'endContainer'];
offset = bookmark[start ? 'startOffset' : 'endOffset'];
if (!container) {
return;
}
if (container.nodeType === 1) {
offset = nodeIndex(container);
container = container.parentNode;
DOM.remove(node);
if (!container.hasChildNodes() && DOM.isBlock(container)) {
container.appendChild(DOM.create('br'));
}
}
bookmark[start ? 'startContainer' : 'endContainer'] = container;
bookmark[start ? 'startOffset' : 'endOffset'] = offset;
}
restoreEndPoint(true);
restoreEndPoint();
const rng = DOM.createRng();
rng.setStart(bookmark.startContainer, bookmark.startOffset);
if (bookmark.endContainer) {
rng.setEnd(bookmark.endContainer, bookmark.endOffset);
}
return Range.normalizeRange(rng);
};
export default {
createBookmark,
resolveBookmark
};

View File

@@ -0,0 +1,265 @@
/**
* 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 RangeUtils from 'tinymce/core/api/dom/RangeUtils';
import TreeWalker from 'tinymce/core/api/dom/TreeWalker';
import VK from 'tinymce/core/api/util/VK';
import ToggleList from '../actions/ToggleList';
import Bookmark from './Bookmark';
import NodeType from './NodeType';
import NormalizeLists from './NormalizeLists';
import Range from './Range';
import Selection from './Selection';
import { flattenListSelection } from '../actions/Indendation';
import { Arr } from '@ephox/katamari';
import { Element, Compare } from '@ephox/sugar';
const findNextCaretContainer = function (editor, rng, isForward, root) {
let node = rng.startContainer;
const offset = rng.startOffset;
let nonEmptyBlocks, walker;
if (node.nodeType === 3 && (isForward ? offset < node.data.length : offset > 0)) {
return node;
}
nonEmptyBlocks = editor.schema.getNonEmptyElements();
if (node.nodeType === 1) {
node = RangeUtils.getNode(node, offset);
}
walker = new TreeWalker(node, root);
// Delete at <li>|<br></li> then jump over the bogus br
if (isForward) {
if (NodeType.isBogusBr(editor.dom, node)) {
walker.next();
}
}
while ((node = walker[isForward ? 'next' : 'prev2']())) {
if (node.nodeName === 'LI' && !node.hasChildNodes()) {
return node;
}
if (nonEmptyBlocks[node.nodeName]) {
return node;
}
if (node.nodeType === 3 && node.data.length > 0) {
return node;
}
}
};
const hasOnlyOneBlockChild = function (dom, elm) {
const childNodes = elm.childNodes;
return childNodes.length === 1 && !NodeType.isListNode(childNodes[0]) && dom.isBlock(childNodes[0]);
};
const unwrapSingleBlockChild = function (dom, elm) {
if (hasOnlyOneBlockChild(dom, elm)) {
dom.remove(elm.firstChild, true);
}
};
const moveChildren = function (dom, fromElm, toElm) {
let node, targetElm;
targetElm = hasOnlyOneBlockChild(dom, toElm) ? toElm.firstChild : toElm;
unwrapSingleBlockChild(dom, fromElm);
if (!NodeType.isEmpty(dom, fromElm, true)) {
while ((node = fromElm.firstChild)) {
targetElm.appendChild(node);
}
}
};
const mergeLiElements = function (dom, fromElm, toElm) {
let node, listNode;
const ul = fromElm.parentNode;
if (!NodeType.isChildOfBody(dom, fromElm) || !NodeType.isChildOfBody(dom, toElm)) {
return;
}
if (NodeType.isListNode(toElm.lastChild)) {
listNode = toElm.lastChild;
}
if (ul === toElm.lastChild) {
if (NodeType.isBr(ul.previousSibling)) {
dom.remove(ul.previousSibling);
}
}
node = toElm.lastChild;
if (node && NodeType.isBr(node) && fromElm.hasChildNodes()) {
dom.remove(node);
}
if (NodeType.isEmpty(dom, toElm, true)) {
dom.$(toElm).empty();
}
moveChildren(dom, fromElm, toElm);
if (listNode) {
toElm.appendChild(listNode);
}
const contains = Compare.contains(Element.fromDom(toElm), Element.fromDom(fromElm));
const nestedLists = contains ? dom.getParents(fromElm, NodeType.isListNode, toElm) : [];
dom.remove(fromElm);
Arr.each(nestedLists, (list) => {
if (NodeType.isEmpty(dom, list) && list !== dom.getRoot()) {
dom.remove(list);
}
});
};
const mergeIntoEmptyLi = function (editor, fromLi, toLi) {
editor.dom.$(toLi).empty();
mergeLiElements(editor.dom, fromLi, toLi);
editor.selection.setCursorLocation(toLi);
};
const mergeForward = function (editor, rng, fromLi, toLi) {
const dom = editor.dom;
if (dom.isEmpty(toLi)) {
mergeIntoEmptyLi(editor, fromLi, toLi);
} else {
const bookmark = Bookmark.createBookmark(rng);
mergeLiElements(dom, fromLi, toLi);
editor.selection.setRng(Bookmark.resolveBookmark(bookmark));
}
};
const mergeBackward = function (editor, rng, fromLi, toLi) {
const bookmark = Bookmark.createBookmark(rng);
mergeLiElements(editor.dom, fromLi, toLi);
const resolvedBookmark = Bookmark.resolveBookmark(bookmark);
editor.selection.setRng(resolvedBookmark);
};
const backspaceDeleteFromListToListCaret = function (editor, isForward) {
const dom = editor.dom, selection = editor.selection;
const selectionStartElm = selection.getStart();
const root = Selection.getClosestListRootElm(editor, selectionStartElm);
const li = dom.getParent(selection.getStart(), 'LI', root);
let ul, rng, otherLi;
if (li) {
ul = li.parentNode;
if (ul === editor.getBody() && NodeType.isEmpty(dom, ul)) {
return true;
}
rng = Range.normalizeRange(selection.getRng(true));
otherLi = dom.getParent(findNextCaretContainer(editor, rng, isForward, root), 'LI', root);
if (otherLi && otherLi !== li) {
if (isForward) {
mergeForward(editor, rng, otherLi, li);
} else {
mergeBackward(editor, rng, li, otherLi);
}
return true;
} else if (!otherLi) {
if (!isForward) {
flattenListSelection(editor);
return true;
}
}
}
return false;
};
const removeBlock = function (dom, block, root) {
const parentBlock = dom.getParent(block.parentNode, dom.isBlock, root);
dom.remove(block);
if (parentBlock && dom.isEmpty(parentBlock)) {
dom.remove(parentBlock);
}
};
const backspaceDeleteIntoListCaret = function (editor, isForward) {
const dom = editor.dom;
const selectionStartElm = editor.selection.getStart();
const root = Selection.getClosestListRootElm(editor, selectionStartElm);
const block = dom.getParent(selectionStartElm, dom.isBlock, root);
if (block && dom.isEmpty(block)) {
const rng = Range.normalizeRange(editor.selection.getRng(true));
const otherLi = dom.getParent(findNextCaretContainer(editor, rng, isForward, root), 'LI', root);
if (otherLi) {
editor.undoManager.transact(function () {
removeBlock(dom, block, root);
ToggleList.mergeWithAdjacentLists(dom, otherLi.parentNode);
editor.selection.select(otherLi, true);
editor.selection.collapse(isForward);
});
return true;
}
}
return false;
};
const backspaceDeleteCaret = function (editor, isForward) {
return backspaceDeleteFromListToListCaret(editor, isForward) || backspaceDeleteIntoListCaret(editor, isForward);
};
const backspaceDeleteRange = function (editor) {
const selectionStartElm = editor.selection.getStart();
const root = Selection.getClosestListRootElm(editor, selectionStartElm);
const startListParent = editor.dom.getParent(selectionStartElm, 'LI,DT,DD', root);
if (startListParent || Selection.getSelectedListItems(editor).length > 0) {
editor.undoManager.transact(function () {
editor.execCommand('Delete');
NormalizeLists.normalizeLists(editor.dom, editor.getBody());
});
return true;
}
return false;
};
const backspaceDelete = function (editor, isForward) {
return editor.selection.isCollapsed() ? backspaceDeleteCaret(editor, isForward) : backspaceDeleteRange(editor);
};
const setup = function (editor) {
editor.on('keydown', function (e) {
if (e.keyCode === VK.BACKSPACE) {
if (backspaceDelete(editor, false)) {
e.preventDefault();
}
} else if (e.keyCode === VK.DELETE) {
if (backspaceDelete(editor, true)) {
e.preventDefault();
}
}
});
};
export default {
setup,
backspaceDelete
};

View File

@@ -0,0 +1,38 @@
/**
* 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 { Editor } from 'tinymce/core/api/Editor';
import { Compare, Replication, Element, Traverse } from '@ephox/sugar';
import SplitList from './SplitList';
import { Indentation } from '../listModel/Indentation';
import { Arr } from '@ephox/katamari';
const outdentDlItem = (editor: Editor, item: Element): void => {
if (Compare.is(item, 'DD')) {
Replication.mutate(item, 'DT');
} else if (Compare.is(item, 'DT')) {
Traverse.parent(item).each((dl) => SplitList.splitList(editor, dl.dom(), item.dom()));
}
};
const indentDlItem = (item: Element): void => {
if (Compare.is(item, 'DT')) {
Replication.mutate(item, 'DD');
}
};
const dlIndentation = (editor: Editor, indentation: Indentation, dlItems: Element[]) => {
if (indentation === Indentation.Indent) {
Arr.each(dlItems, indentDlItem);
} else {
Arr.each(dlItems, (item) => outdentDlItem(editor, item));
}
};
export {
dlIndentation
};

View File

@@ -0,0 +1,38 @@
/**
* 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 VK from 'tinymce/core/api/util/VK';
import Settings from '../api/Settings';
import Delete from './Delete';
import { outdentListSelection, indentListSelection } from '../actions/Indendation';
const setupTabKey = function (editor) {
editor.on('keydown', function (e) {
// Check for tab but not ctrl/cmd+tab since it switches browser tabs
if (e.keyCode !== VK.TAB || VK.metaKeyPressed(e)) {
return;
}
editor.undoManager.transact(() => {
if (e.shiftKey ? outdentListSelection(editor) : indentListSelection(editor)) {
e.preventDefault();
}
});
});
};
const setup = function (editor) {
if (Settings.shouldIndentOnTab(editor)) {
setupTabKey(editor);
}
Delete.setup(editor);
};
export default {
setup
};

View File

@@ -0,0 +1,95 @@
/**
* 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 { Node, Text } from '@ephox/dom-globals';
const isTextNode = function (node: Node): node is Text {
return node && node.nodeType === 3;
};
const isListNode = function (node) {
return node && (/^(OL|UL|DL)$/).test(node.nodeName);
};
const isOlUlNode = function (node) {
return node && (/^(OL|UL)$/).test(node.nodeName);
};
const isListItemNode = function (node) {
return node && /^(LI|DT|DD)$/.test(node.nodeName);
};
const isDlItemNode = function (node) {
return node && /^(DT|DD)$/.test(node.nodeName);
};
const isTableCellNode = function (node) {
return node && /^(TH|TD)$/.test(node.nodeName);
};
const isBr = function (node) {
return node && node.nodeName === 'BR';
};
const isFirstChild = function (node) {
return node.parentNode.firstChild === node;
};
const isLastChild = function (node) {
return node.parentNode.lastChild === node;
};
const isTextBlock = function (editor, node) {
return node && !!editor.schema.getTextBlockElements()[node.nodeName];
};
const isBlock = function (node, blockElements) {
return node && node.nodeName in blockElements;
};
const isBogusBr = function (dom, node) {
if (!isBr(node)) {
return false;
}
if (dom.isBlock(node.nextSibling) && !isBr(node.previousSibling)) {
return true;
}
return false;
};
const isEmpty = function (dom, elm, keepBookmarks?) {
const empty = dom.isEmpty(elm);
if (keepBookmarks && dom.select('span[data-mce-type=bookmark]', elm).length > 0) {
return false;
}
return empty;
};
const isChildOfBody = function (dom, elm) {
return dom.isChildOf(elm, dom.getRoot());
};
export default {
isTextNode,
isListNode,
isOlUlNode,
isDlItemNode,
isListItemNode,
isTableCellNode,
isBr,
isFirstChild,
isLastChild,
isTextBlock,
isBlock,
isBogusBr,
isEmpty,
isChildOfBody
};

View File

@@ -0,0 +1,50 @@
/**
* 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 DOMUtils from 'tinymce/core/api/dom/DOMUtils';
import Tools from 'tinymce/core/api/util/Tools';
import NodeType from './NodeType';
const DOM = DOMUtils.DOM;
const normalizeList = function (dom, ul) {
let sibling;
const parentNode = ul.parentNode;
// Move UL/OL to previous LI if it's the only child of a LI
if (parentNode.nodeName === 'LI' && parentNode.firstChild === ul) {
sibling = parentNode.previousSibling;
if (sibling && sibling.nodeName === 'LI') {
sibling.appendChild(ul);
if (NodeType.isEmpty(dom, parentNode)) {
DOM.remove(parentNode);
}
} else {
DOM.setStyle(parentNode, 'listStyleType', 'none');
}
}
// Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4
if (NodeType.isListNode(parentNode)) {
sibling = parentNode.previousSibling;
if (sibling && sibling.nodeName === 'LI') {
sibling.appendChild(ul);
}
}
};
const normalizeLists = function (dom, element) {
Tools.each(Tools.grep(dom.select('ol,ul', element)), function (ul) {
normalizeList(dom, ul);
});
};
export default {
normalizeList,
normalizeLists
};

View File

@@ -0,0 +1,58 @@
/**
* 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 RangeUtils from 'tinymce/core/api/dom/RangeUtils';
import NodeType from './NodeType';
import { Range, Node } from '@ephox/dom-globals';
interface Point {
container: Node;
offset: number;
}
const getNormalizedPoint = (container: Node, offset: number): Point => {
if (NodeType.isTextNode(container)) {
return { container, offset };
}
const node = RangeUtils.getNode(container, offset);
if (NodeType.isTextNode(node)) {
return {
container: node,
offset: offset >= container.childNodes.length ? node.data.length : 0
};
} else if (node.previousSibling && NodeType.isTextNode(node.previousSibling)) {
return {
container: node.previousSibling,
offset: node.previousSibling.data.length
};
} else if (node.nextSibling && NodeType.isTextNode(node.nextSibling)) {
return {
container: node.nextSibling,
offset: 0
};
}
return { container, offset };
};
const normalizeRange = (rng: Range): Range => {
const outRng = rng.cloneRange();
const rangeStart = getNormalizedPoint(rng.startContainer, rng.startOffset);
outRng.setStart(rangeStart.container, rangeStart.offset);
const rangeEnd = getNormalizedPoint(rng.endContainer, rng.endOffset);
outRng.setEnd(rangeEnd.container, rangeEnd.offset);
return outRng;
};
export default {
getNormalizedPoint,
normalizeRange
};

View File

@@ -0,0 +1,108 @@
/**
* 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 { Node } from '@ephox/dom-globals';
import { Arr, Option } from '@ephox/katamari';
import { HTMLElement } from '@ephox/sand';
import DomQuery from 'tinymce/core/api/dom/DomQuery';
import { Editor } from 'tinymce/core/api/Editor';
import Tools from 'tinymce/core/api/util/Tools';
import NodeType from './NodeType';
const getParentList = function (editor) {
const selectionStart = editor.selection.getStart(true);
return editor.dom.getParent(selectionStart, 'OL,UL,DL', getClosestListRootElm(editor, selectionStart));
};
const isParentListSelected = function (parentList, selectedBlocks) {
return parentList && selectedBlocks.length === 1 && selectedBlocks[0] === parentList;
};
const findSubLists = function (parentList) {
return Tools.grep(parentList.querySelectorAll('ol,ul,dl'), function (elm) {
return NodeType.isListNode(elm);
});
};
const getSelectedSubLists = function (editor) {
const parentList = getParentList(editor);
const selectedBlocks = editor.selection.getSelectedBlocks();
if (isParentListSelected(parentList, selectedBlocks)) {
return findSubLists(parentList);
} else {
return Tools.grep(selectedBlocks, function (elm) {
return NodeType.isListNode(elm) && parentList !== elm;
});
}
};
const findParentListItemsNodes = function (editor, elms) {
const listItemsElms = Tools.map(elms, function (elm) {
const parentLi = editor.dom.getParent(elm, 'li,dd,dt', getClosestListRootElm(editor, elm));
return parentLi ? parentLi : elm;
});
return DomQuery.unique(listItemsElms);
};
const getSelectedListItems = function (editor) {
const selectedBlocks = editor.selection.getSelectedBlocks();
return Tools.grep(findParentListItemsNodes(editor, selectedBlocks), function (block) {
return NodeType.isListItemNode(block);
});
};
const getSelectedDlItems = (editor: Editor): Node[] => {
return Arr.filter(getSelectedListItems(editor), NodeType.isDlItemNode);
};
const getClosestListRootElm = function (editor, elm) {
const parentTableCell = editor.dom.getParents(elm, 'TD,TH');
const root = parentTableCell.length > 0 ? parentTableCell[0] : editor.getBody();
return root;
};
const findLastParentListNode = (editor: Editor, elm: Node): Option<Node> => {
const parentLists = editor.dom.getParents(elm, 'ol,ul', getClosestListRootElm(editor, elm));
return Arr.last(parentLists);
};
const getSelectedLists = (editor: Editor): Node[] => {
const firstList = findLastParentListNode(editor, editor.selection.getStart());
const subsequentLists = Arr.filter(editor.selection.getSelectedBlocks(), NodeType.isOlUlNode);
return firstList.toArray().concat(subsequentLists);
};
const getSelectedListRoots = (editor: Editor): Node[] => {
const selectedLists = getSelectedLists(editor);
return getUniqueListRoots(editor, selectedLists);
};
const getUniqueListRoots = (editor: Editor, lists: Node[]): Node[] => {
const listRoots = Arr.map(lists, (list) => findLastParentListNode(editor, list).getOr(list));
return DomQuery.unique(listRoots);
};
const isList = (editor: Editor): boolean => {
const list = getParentList(editor);
return HTMLElement.isPrototypeOf(list);
};
export default {
isList,
getParentList,
getSelectedSubLists,
getSelectedListItems,
getClosestListRootElm,
getSelectedDlItems,
getSelectedListRoots
};

View File

@@ -0,0 +1,59 @@
/**
* 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 DOMUtils from 'tinymce/core/api/dom/DOMUtils';
import NodeType from './NodeType';
import { createTextBlock } from './TextBlock';
import Tools from 'tinymce/core/api/util/Tools';
const DOM = DOMUtils.DOM;
const splitList = function (editor, ul, li) {
let tmpRng, fragment, bookmarks, node, newBlock;
const removeAndKeepBookmarks = function (targetNode) {
Tools.each(bookmarks, function (node) {
targetNode.parentNode.insertBefore(node, li.parentNode);
});
DOM.remove(targetNode);
};
bookmarks = DOM.select('span[data-mce-type="bookmark"]', ul);
newBlock = createTextBlock(editor, li);
tmpRng = DOM.createRng();
tmpRng.setStartAfter(li);
tmpRng.setEndAfter(ul);
fragment = tmpRng.extractContents();
for (node = fragment.firstChild; node; node = node.firstChild) {
if (node.nodeName === 'LI' && editor.dom.isEmpty(node)) {
DOM.remove(node);
break;
}
}
if (!editor.dom.isEmpty(fragment)) {
DOM.insertAfter(fragment, ul);
}
DOM.insertAfter(newBlock, ul);
if (NodeType.isEmpty(editor.dom, li.parentNode)) {
removeAndKeepBookmarks(li.parentNode);
}
DOM.remove(li);
if (NodeType.isEmpty(editor.dom, ul)) {
DOM.remove(ul);
}
};
export default {
splitList
};

View File

@@ -0,0 +1,75 @@
/**
* 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 Env from 'tinymce/core/api/Env';
import NodeType from './NodeType';
import { DocumentFragment, Node } from '@ephox/dom-globals';
import { Editor } from 'tinymce/core/api/Editor';
const createTextBlock = (editor: Editor, contentNode: Node): DocumentFragment => {
const dom = editor.dom;
const blockElements = editor.schema.getBlockElements();
const fragment = dom.createFragment();
let node, textBlock, blockName, hasContentNode;
if (editor.settings.forced_root_block) {
blockName = editor.settings.forced_root_block;
}
if (blockName) {
textBlock = dom.create(blockName);
if (textBlock.tagName === editor.settings.forced_root_block) {
dom.setAttribs(textBlock, editor.settings.forced_root_block_attrs);
}
if (!NodeType.isBlock(contentNode.firstChild, blockElements)) {
fragment.appendChild(textBlock);
}
}
if (contentNode) {
while ((node = contentNode.firstChild)) {
const nodeName = node.nodeName;
if (!hasContentNode && (nodeName !== 'SPAN' || node.getAttribute('data-mce-type') !== 'bookmark')) {
hasContentNode = true;
}
if (NodeType.isBlock(node, blockElements)) {
fragment.appendChild(node);
textBlock = null;
} else {
if (blockName) {
if (!textBlock) {
textBlock = dom.create(blockName);
fragment.appendChild(textBlock);
}
textBlock.appendChild(node);
} else {
fragment.appendChild(node);
}
}
}
}
if (!editor.settings.forced_root_block) {
fragment.appendChild(dom.create('br'));
} else {
// BR is needed in empty blocks on non IE browsers
if (!hasContentNode && (!Env.ie || Env.ie > 10)) {
textBlock.appendChild(dom.create('br', { 'data-mce-bogus': '1' }));
}
}
return fragment;
};
export {
createTextBlock
};

View File

@@ -0,0 +1,109 @@
/**
* 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 { Document } from '@ephox/dom-globals';
import { Arr, Option, Options } from '@ephox/katamari';
import { Attr, Css, Element, Insert, InsertAll, Node, Replication } from '@ephox/sugar';
import { Entry } from './Entry';
import { ListType } from './Util';
interface Segment {
list: Element;
item: Element;
}
const joinSegment = (parent: Segment, child: Segment): void => {
Insert.append(parent.item, child.list);
};
const joinSegments = (segments: Segment[]): void => {
for (let i = 1; i < segments.length; i++) {
joinSegment(segments[i - 1], segments[i]);
}
};
const appendSegments = (head: Segment[], tail: Segment[]): void => {
Options.liftN([ Arr.last(head), Arr.head(tail)], joinSegment);
};
const createSegment = (scope: Document, listType: ListType): Segment => {
const segment: Segment = {
list: Element.fromTag(listType, scope),
item: Element.fromTag('li', scope)
};
Insert.append(segment.list, segment.item);
return segment;
};
const createSegments = (scope: Document, entry: Entry, size: number): Segment[] => {
const segments: Segment[] = [];
for (let i = 0; i < size; i++) {
segments.push(createSegment(scope, entry.listType));
}
return segments;
};
const populateSegments = (segments: Segment[], entry: Entry): void => {
for (let i = 0; i < segments.length - 1; i++) {
Css.set(segments[i].item, 'list-style-type', 'none');
}
Arr.last(segments).each((segment) => {
Attr.setAll(segment.list, entry.listAttributes);
Attr.setAll(segment.item, entry.itemAttributes);
InsertAll.append(segment.item, entry.content);
});
};
const normalizeSegment = (segment: Segment, entry: Entry): void => {
if (Node.name(segment.list) !== entry.listType) {
segment.list = Replication.mutate(segment.list, entry.listType);
}
Attr.setAll(segment.list, entry.listAttributes);
};
const createItem = (scope: Document, attr: Record<string, any>, content: Element[]): Element => {
const item = Element.fromTag('li', scope);
Attr.setAll(item, attr);
InsertAll.append(item, content);
return item;
};
const appendItem = (segment: Segment, item: Element): void => {
Insert.append(segment.list, item);
segment.item = item;
};
const writeShallow = (scope: Document, cast: Segment[], entry: Entry): Segment[] => {
const newCast = cast.slice(0, entry.depth);
Arr.last(newCast).each((segment) => {
const item = createItem(scope, entry.itemAttributes, entry.content);
appendItem(segment, item);
normalizeSegment(segment, entry);
});
return newCast;
};
const writeDeep = (scope: Document, cast: Segment[], entry: Entry): Segment[] => {
const segments = createSegments(scope, entry, entry.depth - cast.length);
joinSegments(segments);
populateSegments(segments, entry);
appendSegments(cast, segments);
return cast.concat(segments);
};
const composeList = (scope: Document, entries: Entry[]): Option<Element> => {
const cast: Segment[] = Arr.foldl(entries, (cast, entry) => {
return entry.depth > cast.length ? writeDeep(scope, cast, entry) : writeShallow(scope, cast, entry);
}, []);
return Arr.head(cast).map((segment) => segment.list);
};
export { composeList };

View File

@@ -0,0 +1,67 @@
/**
* 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 { Element, Traverse, Replication, Attr, Node } from '@ephox/sugar';
import { Arr, Option } from '@ephox/katamari';
import { hasLastChildList, ListType } from './Util';
/*
General workflow: Parse lists to entries -> Manipulate entries -> Compose entries to lists
0-------1---2--------->Depth
<ol> |
<li>a</li> | Entry { depth: 1, content: [a], listType: ListType.OL, ... }
<li>b | Entry { depth: 1, content: [b], listType: ListType.OL, ... }
<ul> |
<li>c</li> | Entry { depth: 2, content: [c], listType: ListType.UL, ... }
</ul> |
</li> |
</ol> |
0-------1---2--------->Depth
*/
export interface Entry {
depth: number;
content: Element[];
isSelected: boolean;
listType: ListType;
listAttributes: Record<string, any>;
itemAttributes: Record<string, any>;
}
const isIndented = (entry: Entry) => {
return entry.depth > 0;
};
const isSelected = (entry: Entry) => {
return entry.isSelected;
};
const cloneItemContent = (li: Element): Element[] => {
const children = Traverse.children(li);
const content = hasLastChildList(li) ? children.slice(0, -1) : children;
return Arr.map(content, Replication.deep);
};
const createEntry = (li: Element, depth: number, isSelected: boolean): Option<Entry> => {
return Traverse.parent(li).map((list) => {
return {
depth,
isSelected,
content: cloneItemContent(li),
itemAttributes: Attr.clone(li),
listAttributes: Attr.clone(list),
listType: Node.name(list) as ListType
};
});
};
export {
createEntry,
isIndented,
isSelected
};

View File

@@ -0,0 +1,29 @@
/**
* 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 { Entry } from './Entry';
export const enum Indentation {
Indent = 'Indent',
Outdent = 'Outdent',
Flatten = 'Flatten'
}
export const indentEntry = (indentation: Indentation, entry: Entry): void => {
switch (indentation) {
case Indentation.Indent:
entry.depth ++;
break;
case Indentation.Outdent:
entry.depth --;
break;
case Indentation.Flatten:
entry.depth = 0;
}
};

View File

@@ -0,0 +1,62 @@
/**
* 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 { Arr, Fun, Option, Options } from '@ephox/katamari';
import { Element, Fragment, InsertAll, Remove } from '@ephox/sugar';
import { Editor } from 'tinymce/core/api/Editor';
import Selection from '../core/Selection';
import { composeList } from './ComposeList';
import { Entry, isIndented, isSelected } from './Entry';
import { indentEntry, Indentation } from './Indentation';
import { normalizeEntries } from './NormalizeEntries';
import { EntrySet, ItemSelection, parseLists } from './ParseLists';
import { hasFirstChildList } from './Util';
import { createTextBlock } from '../core/TextBlock';
const outdentedComposer = (editor: Editor, entries: Entry[]): Element[] => {
return Arr.map(entries, (entry) => {
const content = Fragment.fromElements(entry.content);
return Element.fromDom(createTextBlock(editor, content.dom()));
});
};
const indentedComposer = (editor: Editor, entries: Entry[]): Element[] => {
normalizeEntries(entries);
return composeList(editor.contentDocument, entries).toArray();
};
const composeEntries = (editor, entries: Entry[]): Element[] => {
return Arr.bind(Arr.groupBy(entries, isIndented), (entries) => {
const groupIsIndented = Arr.head(entries).map(isIndented).getOr(false);
return groupIsIndented ? indentedComposer(editor, entries) : outdentedComposer(editor, entries);
});
};
const indentSelectedEntries = (entries: Entry[], indentation: Indentation): void => {
Arr.each(Arr.filter(entries, isSelected), (entry) => indentEntry(indentation, entry));
};
const getItemSelection = (editor: Editor): Option<ItemSelection> => {
const selectedListItems = Arr.map(Selection.getSelectedListItems(editor), Element.fromDom);
return Options.liftN([
Arr.find(selectedListItems, Fun.not(hasFirstChildList)),
Arr.find(Arr.reverse(selectedListItems), Fun.not(hasFirstChildList))
], (start, end) => ({ start, end }));
};
const listsIndentation = (editor: Editor, lists: Element[], indentation: Indentation) => {
const entrySets: EntrySet[] = parseLists(lists, getItemSelection(editor));
Arr.each(entrySets, (entrySet) => {
indentSelectedEntries(entrySet.entries, indentation);
InsertAll.before(entrySet.sourceList, composeEntries(editor, entrySet.entries));
Remove.remove(entrySet.sourceList);
});
};
export { listsIndentation };

View File

@@ -0,0 +1,40 @@
/**
* 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 { Entry } from './Entry';
import { Arr, Merger, Option } from '@ephox/katamari';
const cloneListProperties = (target: Entry, source: Entry): void => {
target.listType = source.listType;
target.listAttributes = Merger.merge({}, source.listAttributes);
};
// Closest entry above in the same list
const previousSiblingEntry = (entries: Entry[], start: number): Option<Entry> => {
const depth = entries[start].depth;
for (let i = start - 1; i >= 0; i--) {
if (entries[i].depth === depth) {
return Option.some(entries[i]);
}
if (entries[i].depth < depth) {
break;
}
}
return Option.none();
};
const normalizeEntries = (entries: Entry[]): void => {
Arr.each(entries, (entry, i) => {
previousSiblingEntry(entries, i).each((matchingEntry) => {
cloneListProperties(entry, matchingEntry);
});
});
};
export {
normalizeEntries
};

View File

@@ -0,0 +1,71 @@
/**
* 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 { Arr, Cell, Option } from '@ephox/katamari';
import { Compare, Element, Traverse } from '@ephox/sugar';
import { createEntry, Entry } from './Entry';
import { isList } from './Util';
type Parser = (depth: number, itemSelection: Option<ItemSelection>, selectionState: Cell<boolean>, element: Element) => Entry[];
export interface ItemSelection {
start: Element;
end: Element;
}
export interface EntrySet {
entries: Entry[];
sourceList: Element;
}
const parseItem: Parser = (depth: number, itemSelection: Option<ItemSelection>, selectionState: Cell<boolean>, item: Element): Entry[] => {
return Traverse.firstChild(item).filter(isList).fold(() => {
// Update selectionState (start)
itemSelection.each((selection) => {
if (Compare.eq(selection.start, item)) {
selectionState.set(true);
}
});
const currentItemEntry = createEntry(item, depth, selectionState.get());
// Update selectionState (end)
itemSelection.each((selection) => {
if (Compare.eq(selection.end, item)) {
selectionState.set(false);
}
});
const childListEntries: Entry[] = Traverse.lastChild(item)
.filter(isList)
.map((list) => parseList(depth, itemSelection, selectionState, list))
.getOr([]);
return currentItemEntry.toArray().concat(childListEntries);
}, (list) => parseList(depth, itemSelection, selectionState, list));
};
const parseList: Parser = (depth: number, itemSelection: Option<ItemSelection>, selectionState: Cell<boolean>, list: Element): Entry[] => {
return Arr.bind(Traverse.children(list), (element) => {
const parser = isList(element) ? parseList : parseItem;
const newDepth = depth + 1;
return parser(newDepth, itemSelection, selectionState, element);
});
};
const parseLists = (lists: Element[], itemSelection: Option<ItemSelection>): EntrySet[] => {
const selectionState = Cell(false);
const initialDepth = 0;
return Arr.map(lists, (list) => ({
sourceList: list,
entries: parseList(initialDepth, itemSelection, selectionState, list)
}));
};
export { parseLists };

View File

@@ -0,0 +1,31 @@
/**
* 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 { Element, Traverse, Compare } from '@ephox/sugar';
export const enum ListType {
OL = 'ol',
UL = 'ul'
}
const isList = (el: Element) => {
return Compare.is(el, 'OL,UL');
};
const hasFirstChildList = (el: Element) => {
return Traverse.firstChild(el).map(isList).getOr(false);
};
const hasLastChildList = (el: Element) => {
return Traverse.lastChild(el).map(isList).getOr(false);
};
export {
isList,
hasFirstChildList,
hasLastChildList
};

View File

@@ -0,0 +1,65 @@
/**
* 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 Tools from 'tinymce/core/api/util/Tools';
import NodeType from '../core/NodeType';
const findIndex = function (list, predicate) {
for (let index = 0; index < list.length; index++) {
const element = list[index];
if (predicate(element)) {
return index;
}
}
return -1;
};
const listState = function (editor, listName) {
return function (e) {
const ctrl = e.control;
editor.on('NodeChange', function (e) {
const tableCellIndex = findIndex(e.parents, NodeType.isTableCellNode);
const parents = tableCellIndex !== -1 ? e.parents.slice(0, tableCellIndex) : e.parents;
const lists = Tools.grep(parents, NodeType.isListNode);
ctrl.active(lists.length > 0 && lists[0].nodeName === listName);
});
};
};
const register = function (editor) {
const hasPlugin = function (editor, plugin) {
const plugins = editor.settings.plugins ? editor.settings.plugins : '';
return Tools.inArray(plugins.split(/[ ,]/), plugin) !== -1;
};
if (!hasPlugin(editor, 'advlist')) {
editor.addButton('numlist', {
active: false,
title: 'Numbered list',
cmd: 'InsertOrderedList',
onPostRender: listState(editor, 'OL')
});
editor.addButton('bullist', {
active: false,
title: 'Bullet list',
cmd: 'InsertUnorderedList',
onPostRender: listState(editor, 'UL')
});
}
editor.addButton('indent', {
icon: 'indent',
title: 'Increase indent',
cmd: 'Indent'
});
};
export default {
register
};

View File

@@ -0,0 +1,133 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.ApplyTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Apply DL list to multiple Ps', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>' +
'<p>b</p>' +
'<p>c</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0, 'p:last', 0);
LegacyUnit.execCommand(editor, 'InsertDefinitionList');
LegacyUnit.equal(editor.getContent(),
'<dl>' +
'<dt>a</dt>' +
'<dt>b</dt>' +
'<dt>c</dt>' +
'</dl>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'DT');
});
suite.test('Apply OL list to single P', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertDefinitionList');
LegacyUnit.equal(editor.getContent(), '<dl><dt>a</dt></dl>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'DT');
});
suite.test('Apply DL to P and merge with adjacent lists', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<dl>' +
'<dt>a</dt>' +
'</dl>' +
'<p>b</p>' +
'<dl>' +
'<dt>c</dt>' +
'</dl>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 1);
LegacyUnit.execCommand(editor, 'InsertDefinitionList');
LegacyUnit.equal(editor.getContent(),
'<dl>' +
'<dt>a</dt>' +
'<dt>b</dt>' +
'<dt>c</dt>' +
'</dl>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'DT');
});
suite.test('Indent single DT in DL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<dl>' +
'<dt>a</dt>' +
'</dl>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'dt', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<dl>' +
'<dd>a</dd>' +
'</dl>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'DD');
});
suite.test('Outdent single DD in DL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<dl>' +
'<dd>a</dd>' +
'</dl>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'dd', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<dl>' +
'<dt>a</dt>' +
'</dl>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'DT');
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom],ol[style|class|data-custom],' +
'ul[style|class|data-custom],dl,dt,dd,em,strong,span,#p,div,br',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,39 @@
import { GeneralSteps, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
import ListsPlugin from 'tinymce/plugins/lists/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('browser.tinymce.plugins.lists.ApplyListOnParagraphWithStylesTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
ModernTheme();
ListsPlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const tinyUi = TinyUi(editor);
Pipeline.async({}, [
Logger.t('remove margin from p when applying list on it, but leave other styles', GeneralSteps.sequence([
tinyApis.sSetContent('<p style="color: blue;margin: 30px;margin-right: 30px;margin-bottom: 30px;margin-left: 30px;margin-top: 30px;">test</p>'),
tinyApis.sSetCursor([0, 0], 0),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Bullet list"] button'),
tinyApis.sAssertContent('<ul><li style="color: blue;">test</li></ul>')
])),
Logger.t('remove padding from p when applying list on it, but leave other styles', GeneralSteps.sequence([
tinyApis.sSetContent('<p style="color: red;padding: 30px;padding-right: 30px;padding-bottom: 30px;padding-left: 30px;padding-top: 30px;">test</p>'),
tinyApis.sSetCursor([0, 0], 0),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Bullet list"] button'),
tinyApis.sAssertContent('<ul><li style="color: red;">test</li></ul>')
]))
], onSuccess, onFailure);
}, {
indent: false,
plugins: 'lists',
toolbar: 'numlist bullist',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,984 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Env from 'tinymce/core/api/Env';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.ApplyTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Apply UL list to single P', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(), '<ul><li>a</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Apply UL list to single empty P', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p><br></p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(LegacyUnit.trimBrs(editor.getContent({ format: 'raw' })), '<ul><li></li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Apply UL list to multiple Ps', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>' +
'<p>b</p>' +
'<p>c</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0, 'p:last', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply OL list to single P', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(), '<ol><li>a</li></ol>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Apply OL list to single empty P', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p><br></p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(LegacyUnit.trimBrs(editor.getContent({ format: 'raw' })), '<ol><li></li></ol>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Apply OL list to multiple Ps', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>' +
'<p>b</p>' +
'<p>c</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0, 'p:last', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply OL to UL list', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test(
'Apply OL to UL list with collapsed selection',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test('Apply UL to OL list', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply UL to OL list collapsed selection', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply UL to P and merge with adjacent lists', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<p>b</p>' +
'<ul>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply UL to OL and merge with adjacent lists', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<ol><li>b</li></ol>' +
'<ul>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol li', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply OL to P and merge with adjacent lists', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'</ol>' +
'<p>b</p>' +
'<ol>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test('Apply OL to UL and merge with adjacent lists', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>1a</li>' +
'<li>1b</li>' +
'</ol>' +
'<ul><li>2a</li><li>2b</li></ul>' +
'<ol>' +
'<li>3a</li>' +
'<li>3b</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>1a</li>' +
'<li>1b</li>' +
'<li>2a</li>' +
'<li>2b</li>' +
'<li>3a</li>' +
'<li>3b</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
});
suite.test(
'Apply OL to UL and DO not merge with adjacent lists because styles are different (exec has style)',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'</ol>' +
'<ul><li>b</li></ul>' +
'<ol>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList', null, { 'list-style-type': 'lower-alpha' });
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'</ol>' +
'<ol style="list-style-type: lower-alpha;"><li>b</li></ol>' +
'<ol>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test(
'Apply OL to P and DO not merge with adjacent lists because styles are different (exec has style)',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'</ol>' +
'<p>b</p>' +
'<ol>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList', null, { 'list-style-type': 'lower-alpha' });
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'</ol>' +
'<ol style="list-style-type: lower-alpha;"><li>b</li></ol>' +
'<ol>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test(
'Apply OL to UL and DO not merge with adjacent lists because styles are different (original has style)',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol style="list-style-type: upper-roman;">' +
'<li>a</li>' +
'</ol>' +
'<ul><li>b</li></ul>' +
'<ol style="list-style-type: upper-roman;">' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol style="list-style-type: upper-roman;">' +
'<li>a</li>' +
'</ol>' +
'<ol><li>b</li></ol>' +
'<ol style="list-style-type: upper-roman;">' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test(
'Apply OL to UL should merge with adjacent lists because styles are the same (both have roman)',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol style="list-style-type: upper-roman;">' +
'<li>a</li>' +
'</ol>' +
'<ul><li>b</li></ul>' +
'<ol style="list-style-type: upper-roman;">' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList', false, { 'list-style-type': 'upper-roman' });
LegacyUnit.equal(editor.getContent(),
'<ol style="list-style-type: upper-roman;">' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test(
'Apply OL to UL should merge with above list because styles are the same (both have lower-roman), but not below list',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol style="list-style-type: lower-roman;">' +
'<li>a</li>' +
'</ol>' +
'<ul><li>b</li></ul>' +
'<ol style="list-style-type: upper-roman;">' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList', false, { 'list-style-type': 'lower-roman' });
LegacyUnit.equal(editor.getContent(),
'<ol style="list-style-type: lower-roman;">' +
'<li>a</li>' +
'<li>b</li>' +
'</ol>' +
'<ol style="list-style-type: upper-roman;">' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test(
'Apply OL to UL should merge with below lists because styles are the same (both have roman), but not above list',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol style="list-style-type: upper-roman;">' +
'<li>a</li>' +
'</ol>' +
'<ul><li>b</li></ul>' +
'<ol style="list-style-type: lower-roman;">' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList', false, { 'list-style-type': 'lower-roman' });
LegacyUnit.equal(editor.getContent(),
'<ol style="list-style-type: upper-roman;">' +
'<li>a</li>' +
'</ol>' +
'<ol style="list-style-type: lower-roman;">' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test(
'Apply OL to UL and DO not merge with adjacent lists because classes are different',
function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol class="a">' +
'<li>a</li>' +
'</ol>' +
'<ul><li>b</li></ul>' +
'<ol class="b">' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol class="a">' +
'<li>a</li>' +
'</ol>' +
'<ol><li>b</li></ol>' +
'<ol class="b">' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test('Apply UL list to single text line', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'a'
);
editor.focus();
LegacyUnit.setSelection(editor, 'body', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(), '<ul><li>a</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
editor.settings.forced_root_block = 'p';
});
suite.test('Apply UL list to single text line with BR', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'a<br>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'body', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(), '<ul><li>a</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
editor.settings.forced_root_block = 'p';
});
suite.test('Apply UL list to multiple lines separated by BR', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'a<br>' +
'b<br>' +
'c'
);
editor.focus();
editor.execCommand('SelectAll');
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
editor.settings.forced_root_block = 'p';
});
suite.test(
'Apply UL list to multiple lines separated by BR and with trailing BR',
function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'a<br>' +
'b<br>' +
'c<br>'
);
editor.focus();
editor.execCommand('SelectAll');
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
}
);
suite.test('Apply UL list to multiple formatted lines separated by BR', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'<strong>a</strong><br>' +
'<span>b</span><br>' +
'<em>c</em>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'strong', 0, 'em', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li><strong>a</strong></li>' +
'<li><span>b</span></li>' +
'<li><em>c</em></li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'STRONG');
// Old IE will return the end LI not a big deal
LegacyUnit.equal(editor.selection.getEnd().nodeName, Env.ie && Env.ie < 9 ? 'LI' : 'EM');
});
// Ignore on IE 7, 8 this is a known bug not worth fixing
if (!Env.ie || Env.ie > 8) {
suite.test('Apply UL list to br line and text block line', function (editor) {
editor.settings.forced_root_block = false;
editor.setContent(
'a' +
'<p>b</p>'
);
const rng = editor.dom.createRng();
rng.setStart(editor.getBody().firstChild, 0);
rng.setEnd(editor.getBody().lastChild.firstChild, 1);
editor.selection.setRng(rng);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
LegacyUnit.equal(editor.selection.getEnd().nodeName, 'LI');
});
}
suite.test('Apply UL list to text block line and br line', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'<p>a</p>' +
'b'
);
editor.focus();
const rng = editor.dom.createRng();
rng.setStart(editor.getBody().firstChild.firstChild, 0);
rng.setEnd(editor.getBody().lastChild, 1);
editor.selection.setRng(rng);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'LI');
LegacyUnit.equal(editor.selection.getEnd().nodeName, 'LI');
});
suite.test('Apply UL list to all BR lines (SelectAll)', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = (
'a<br>' +
'b<br>' +
'c<br>'
);
editor.focus();
editor.execCommand('SelectAll');
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.settings.forced_root_block = 'p';
});
suite.test('Apply UL list to all P lines (SelectAll)', function (editor) {
editor.getBody().innerHTML = (
'<p>a</p>' +
'<p>b</p>' +
'<p>c</p>'
);
editor.focus();
editor.execCommand('SelectAll');
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
});
suite.test('Apply UL list to single P', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(), '<ul><li>a</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Apply UL list to more than two paragraphs', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>a</p>' +
'<p>b</p>' +
'<p>c</p>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p:nth-child(1)', 0, 'p:nth-child(3)', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList', false, { 'list-style-type': null });
LegacyUnit.equal(editor.getContent(), '<ul><li>a</li><li>b</li><li>c</li></ul>');
});
suite.test('Apply UL with custom attributes', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs('<p>a</p>');
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList', false, {
'list-attributes': {
'class': 'a',
'data-custom': 'c1'
}
});
LegacyUnit.equal(editor.getContent(), '<ul class="a" data-custom="c1"><li>a</li></ul>');
});
suite.test('Apply UL and LI with custom attributes', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs('<p>a</p>');
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList', false, {
'list-attributes': {
'class': 'a',
'data-custom': 'c1'
},
'list-item-attributes': {
'class': 'b',
'data-custom1': 'c2',
'data-custom2': ''
}
});
LegacyUnit.equal(editor.getContent(), '<ul class="a" data-custom="c1"><li class="b" data-custom1="c2" data-custom2="">a</li></ul>');
});
suite.test('Handle one empty unordered list items without error', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li></li>' +
'</ul>'
);
editor.execCommand('SelectAll');
LegacyUnit.setSelection(editor, 'li:first', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getBody().innerHTML,
'<p>a</p>' +
'<p>b</p>' +
'<p><br data-mce-bogus="1"></p>'
);
});
suite.test('Handle several empty unordered list items without error', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li></li>' +
'<li>c</li>' +
'<li></li>' +
'<li>d</li>' +
'<li></li>' +
'<li>e</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:first', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getBody().innerHTML,
'<p>a</p>' +
'<p>b</p>' +
'<p><br data-mce-bogus=\"1\"></p>' +
'<p>c</p>' +
'<p><br data-mce-bogus=\"1\"></p>' +
'<p>d</p>' +
'<p><br data-mce-bogus=\"1\"></p>' +
'<p>e</p>'
);
});
suite.test('Handle one empty ordered list items without error', function (editor) {
editor.getBody().innerHTML = (
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li></li>' +
'</ol>'
);
editor.execCommand('SelectAll');
LegacyUnit.setSelection(editor, 'li:first', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getBody().innerHTML,
'<p>a</p>' +
'<p>b</p>' +
'<p><br data-mce-bogus="1"></p>'
);
});
suite.test('Handle several empty ordered list items without error', function (editor) {
editor.getBody().innerHTML = (
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li></li>' +
'<li>c</li>' +
'<li></li>' +
'<li>d</li>' +
'<li></li>' +
'<li>e</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:first', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getBody().innerHTML,
'<p>a</p>' +
'<p>b</p>' +
'<p><br data-mce-bogus=\"1\"></p>' +
'<p>c</p>' +
'<p><br data-mce-bogus=\"1\"></p>' +
'<p>d</p>' +
'<p><br data-mce-bogus=\"1\"></p>' +
'<p>e</p>'
);
});
suite.test('Apply list on paragraphs with list between', function (editor) {
editor.getBody().innerHTML = (
'<p>a</p>' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'<p>c</p>'
);
editor.execCommand('SelectAll');
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getBody().innerHTML, '<ul><li>a</li></ul><ol><li>b</li></ol><ul><li>c</li></ul>');
});
suite.test('Apply unordered list on children on a fully selected ordered list', function (editor) {
editor.getBody().innerHTML = (
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c</li>' +
'</ol>'
);
editor.execCommand('SelectAll');
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getBody().innerHTML, '<ul><li>a<ul><li>b</li></ul></li><li>c</li></ul>');
});
suite.test('Apply unordered list on empty table cell', function (editor) {
editor.getBody().innerHTML = (
'<table>' +
'<tbody>' +
'<tr>' +
'<td>' +
'<br />' +
'</td>' +
'</tr>' +
'</tbody>' +
'</table>'
);
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('td')[0], 0);
rng.setEnd(editor.dom.select('td')[0], 1);
editor.selection.setRng(rng);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getBody().innerHTML, '<table><tbody><tr><td><ul><li><br></li></ul></td></tr></tbody></table>');
});
suite.test('Apply unordered list on table cell with two lines br', function (editor) {
editor.getBody().innerHTML = (
'<table>' +
'<tbody>' +
'<tr>' +
'<td>' +
'a<br>b' +
'</td>' +
'</tr>' +
'</tbody>' +
'</table>'
);
const rng = editor.dom.createRng();
rng.setStart(editor.dom.select('td')[0].firstChild, 0);
rng.setEnd(editor.dom.select('td')[0].firstChild, 0);
editor.selection.setRng(rng);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getBody().innerHTML, '<table><tbody><tr><td><ul><li>a</li></ul>b</td></tr></tbody></table>');
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom|data-custom1|data-custom2],ol[style|class|data-custom|data-custom1|data-custom2],' +
'ul[style|class|data-custom|data-custom1|data-custom2],dl,dt,dd,em,strong,span,#p,div,br',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,62 @@
import { GeneralSteps, Keys, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyActions, TinyApis, TinyLoader } from '@ephox/mcagar';
import ListsPlugin from 'tinymce/plugins/lists/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('Browser Test: .RemoveTrailingBlockquoteTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
ModernTheme();
ListsPlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const tinyActions = TinyActions(editor);
Pipeline.async({}, [
Logger.t('backspace from p inside div into li', GeneralSteps.sequence([
tinyApis.sFocus,
tinyApis.sSetContent('<ul><li>a</li></ul><div><p><br /></p></div>'),
tinyApis.sSetCursor([1, 0, 0], 0),
tinyActions.sContentKeystroke(Keys.backspace(), { }),
tinyApis.sAssertContent('<ul><li>a</li></ul>')
])),
Logger.t('backspace from p inside blockquote into li', GeneralSteps.sequence([
tinyApis.sFocus,
tinyApis.sSetContent('<ul><li>a</li></ul><blockquote><p><br /></p></blockquote>'),
tinyApis.sSetCursor([1, 0, 0], 0),
tinyActions.sContentKeystroke(Keys.backspace(), { }),
tinyApis.sAssertContent('<ul><li>a</li></ul>')
])),
Logger.t('backspace from b inside p inside blockquote into li', GeneralSteps.sequence([
tinyApis.sFocus,
tinyApis.sSetContent('<ul><li>a</li></ul><blockquote><p><b><br /></b></p></blockquote>'),
tinyApis.sSetCursor([1, 0, 0, 0], 0),
tinyActions.sContentKeystroke(Keys.backspace(), { }),
tinyApis.sAssertContent('<ul><li>a</li></ul>')
])),
Logger.t('backspace from span inside p inside blockquote into li', GeneralSteps.sequence([
tinyApis.sFocus,
tinyApis.sSetContent('<ul><li>a</li></ul><blockquote><p><span class="x"><br /></span></p></blockquote>'),
tinyApis.sSetCursor([1, 0, 0, 0], 0),
tinyActions.sContentKeystroke(Keys.backspace(), { }),
tinyApis.sAssertContent('<ul><li>a</li></ul>')
])),
Logger.t('backspace from p into li', GeneralSteps.sequence([
tinyApis.sFocus,
tinyApis.sSetContent('<ul><li>a</li></ul><p><br /></p>'),
tinyApis.sSetCursor([1, 0], 0),
tinyActions.sContentKeystroke(Keys.backspace(), { }),
tinyApis.sAssertContent('<ul><li>a</li></ul>')
]))
], onSuccess, onFailure);
}, {
indent: false,
plugins: 'lists',
toolbar: '',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,73 @@
import { Pipeline } from '@ephox/agar';
import { LegacyUnit } from '@ephox/mcagar';
import DomQuery from 'tinymce/core/api/dom/DomQuery';
import EditorManager from 'tinymce/core/api/EditorManager';
import Plugin from 'tinymce/plugins/lists/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
import { UnitTest } from '@ephox/bedrock';
import { document } from '@ephox/dom-globals';
UnitTest.asynctest('tinymce.lists.browser.BackspaceDeleteInlineTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
ModernTheme();
suite.test('Backspace at beginning of LI on body UL', function (editor) {
editor.focus();
editor.selection.setCursorLocation(editor.getBody().firstChild.firstChild, 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(DomQuery('#lists ul').length, 3);
LegacyUnit.equal(DomQuery('#lists li').length, 3);
});
suite.test('Delete at end of LI on body UL', function (editor) {
editor.focus();
editor.selection.setCursorLocation(editor.getBody().firstChild.firstChild, 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(DomQuery('#lists ul').length, 3);
LegacyUnit.equal(DomQuery('#lists li').length, 3);
});
const teardown = function (editor, div) {
editor.remove();
div.parentNode.removeChild(div);
};
const setup = function (success, failure) {
const div = document.createElement('div');
div.innerHTML = (
'<div id="lists">' +
'<ul><li>before</li></ul>' +
'<ul id="inline"><li>x</li></ul>' +
'<ul><li>after</li></ul>' +
'</div>'
);
document.body.appendChild(div);
EditorManager.init({
selector: '#inline',
inline: true,
add_unload_trigger: false,
skin: false,
plugins: 'lists',
disable_nodechange: true,
init_instance_callback (editor) {
Pipeline.async({}, suite.toSteps(editor), function () {
teardown(editor, div);
success();
}, failure);
},
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,font-style,text-decoration,float,' +
'margin,margin-top,margin-right,margin-bottom,margin-left,display,position,top,left,list-style-type'
}
});
};
setup(success, failure);
});

View File

@@ -0,0 +1,917 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.BackspaceDeleteTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Backspace at beginning of single LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<p>a</p>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Backspace at beginning of first LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<p>a</p>' +
'<ul>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Backspace at beginning of middle LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>ab</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at beginning of start LI in UL inside UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>ab' +
'<ul>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at beginning of middle LI in UL inside UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>bc</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at beginning of single LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<p>a</p>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Backspace at beginning of first LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<p>a</p>' +
'<ul>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Backspace at beginning of middle LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>ab</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at beginning of start LI in UL inside UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>ab' +
'<ul>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at beginning of middle LI in UL inside UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>bc</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at beginning of LI with empty LI above in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(3)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'b');
});
suite.test('Backspace at beginning of LI with BR padded empty LI above in UL', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li><br></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(3)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'b');
});
suite.test('Backspace at empty LI (IE)', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'a');
});
suite.test('Backspace at beginning of LI with empty LI with STRING and BR above in UL', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li><strong><br></strong></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(3)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'b');
});
suite.test('Backspace at nested LI with adjacent BR', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>1' +
'<ul>' +
'<li>' +
'<br>' +
'<ul>' +
'<li>2</li>' +
'</ul>' +
'</li>' +
'</ul>' +
'</li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul ul ul li', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(), '<ul><li>1<ul><li>2</li></ul></li><li>3</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at LI selected with triple-click in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b' +
'<ul>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(1)', 0, 'li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(LegacyUnit.trimBrs(editor.getContent()),
'<ul>' +
'<li>b' +
'<ul>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at partially selected list', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<p>abc</p>' +
'<ul>' +
'<li>a</li>' +
'<li>b' +
'<ul>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 1, 'li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(LegacyUnit.trimBrs(editor.getContent()),
'<p>ab</p>' +
'<ul>' +
'<li style="list-style-type: none;">' +
'<ul>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
// Delete
suite.test('Delete at end of single LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at end of first LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>ab</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at end of middle LI in UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>bc</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at end of start LI in UL inside UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>bc</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at end of middle LI in UL inside UL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li:nth-child(2)', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>cd</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at end of LI before empty LI', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'a');
});
suite.test('Delete at end of LI before BR padded empty LI', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li><br></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'a');
});
suite.test('Delete at end of LI before empty LI with STRONG', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>a</li>' +
'<li><strong><br></strong></li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().innerHTML, 'a');
});
suite.test('Delete at nested LI with adjacent BR', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>1' +
'<ul>' +
'<li>' +
'<br>' +
'<ul>' +
'<li>2</li>' +
'</ul>' +
'</li>' +
'</ul>' +
'</li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
editor.selection.setCursorLocation(editor.$('ul ul li')[0], 0);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(editor.getContent(), '<ul><li>1<ul><li>2</li></ul></li><li>3</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at BR before text in LI', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>1</li>' +
'<li>2<br></li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
editor.selection.setCursorLocation(editor.$('li')[1], 1);
editor.plugins.lists.backspaceDelete(false);
LegacyUnit.equal(editor.getContent(), '<ul><li>1</li><li>2</li><li>3</li></ul>');
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace merge li elements', function (editor) {
// IE allows you to place the caret inside a LI without children
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li></li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
LegacyUnit.equal(editor.selection.getRng(true).startContainer.nodeType, 3, 'Should be a text node');
});
suite.test('Backspace at block inside li element into li without block element', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>1</li>' +
'<li><p>2</p></li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li>12</li>' +
'<li>3</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace at block inside li element into li with block element', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li><p>1</p></li>' +
'<li><p>2</p></li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2) p', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li><p>12</p></li>' +
'<li>3</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Backspace at block inside li element into li with multiple block elements', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li><p>1</p><p>2</p></li>' +
'<li><p>3</p></li>' +
'<li>4</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2) p', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li><p>1</p><p>2</p>3</li>' +
'<li>4</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete at block inside li element into li without block element', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li><p>1</p></li>' +
'<li>2</li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li><p>12</p></li>' +
'<li>3</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Delete at block inside li element into li with block element', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li><p>1</p></li>' +
'<li><p>2</p></li>' +
'<li>3</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(1) p', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li><p>12</p></li>' +
'<li>3</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Delete at block inside li element into li with multiple block elements', function (editor) {
editor.getBody().innerHTML = (
'<ul>' +
'<li>1</li>' +
'<li><p>2</p><p>3</p></li>' +
'<li>4</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(1)', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(
editor.getContent(),
'<ul>' +
'<li>1<p>2</p><p>3</p></li>' +
'<li>4</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Backspace from indented list', function (editor) {
editor.getBody().innerHTML = (
'<ol>' +
'<li>a' +
'<ol>' +
'<li style="list-style-type: none;">' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol li ol li ol li:nth-child(1)', 0);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(
editor.getContent(),
'<ol>' +
'<li>ab</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Delete into indented list', function (editor) {
editor.getBody().innerHTML = (
'<ol>' +
'<li>a' +
'<ol>' +
'<li style="list-style-type: none;">' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol li:nth-child(1)', 1);
editor.plugins.lists.backspaceDelete(true);
LegacyUnit.equal(
editor.getContent(),
'<ol>' +
'<li>ab</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom],ol[style|class|data-custom],' +
'ul[style|class|data-custom],dl,dt,dd,em,strong,span,#p,div,br',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
content_style: '.mce-content-body { line-height: normal; }', // Breaks tests in phantomjs unless we have this
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,83 @@
import { GeneralSteps, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
import ListsPlugin from 'tinymce/plugins/lists/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('browser.tinymce.plugins.lists.ChangeListStyleTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
ModernTheme();
ListsPlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const tinyUi = TinyUi(editor);
Pipeline.async({}, [
Logger.t('ul to ol, cursor only in parent', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li>a</li><ul><li>b</li></ul></ul>'),
tinyApis.sSetCursor([0, 0, 0], 0),
tinyUi.sClickOnToolbar('click numlist button', 'div[aria-label="Numbered list"] > button'),
tinyApis.sAssertContent('<ol><li>a</li><ul><li>b</li></ul></ol>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 0, 0], 0)
])),
Logger.t('ul to ol, selection from parent to sublist', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li>a</li><ol><li>b</li></ol></ul>'),
tinyApis.sSetSelection([0, 0, 0], 0, [0, 1, 0, 0], 1),
tinyUi.sClickOnToolbar('click numlist button', 'div[aria-label="Numbered list"] > button'),
tinyApis.sAssertContent('<ol><li>a</li><ol><li>b</li></ol></ol>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 1, 0, 0], 1)
])),
Logger.t('ol to ul, cursor only in parent', GeneralSteps.sequence([
tinyApis.sSetContent('<ol><li>a</li><ol><li>b</li></ol></ol>'),
tinyApis.sSetCursor([0, 0, 0], 0),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Bullet list"] > button'),
tinyApis.sAssertContent('<ul><li>a</li><ol><li>b</li></ol></ul>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 0, 0], 0)
])),
Logger.t('ol to ul, selection from parent to sublist', GeneralSteps.sequence([
tinyApis.sSetContent('<ol><li>a</li><ul><li>b</li></ul></ol>'),
tinyApis.sSetSelection([0, 0, 0], 0, [0, 1, 0, 0], 1),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Bullet list"] > button'),
tinyApis.sAssertContent('<ul><li>a</li><ul><li>b</li></ul></ul>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 1, 0, 0], 1)
])),
Logger.t('alpha to ol, cursor only in parent', GeneralSteps.sequence([
tinyApis.sSetContent('<ul style="list-style-type: lower-alpha;"><li>a</li><ol style="list-style-type: lower-alpha;"><li>b</li></ol></ul>'),
tinyApis.sSetCursor([0, 0, 0], 0),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Numbered list"] > button'),
tinyApis.sAssertContent('<ol><li>a</li><ol style="list-style-type: lower-alpha;"><li>b</li></ol></ol>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 0, 0], 0)
])),
Logger.t('alpha to ol, selection from parent to sublist', GeneralSteps.sequence([
tinyApis.sSetContent('<ul style="list-style-type: lower-alpha;"><li>a</li><ol style="list-style-type: lower-alpha;"><li>b</li></ol></ul>'),
tinyApis.sSetSelection([0, 0, 0], 0, [0, 1, 0, 0], 1),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Numbered list"] > button'),
tinyApis.sAssertContent('<ol><li>a</li><ol><li>b</li></ol></ol>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 1, 0, 0], 1)
])),
Logger.t('alpha to ul, cursor only in parent', GeneralSteps.sequence([
tinyApis.sSetContent('<ol style="list-style-type: lower-alpha;"><li>a</li><ol style="list-style-type: lower-alpha;"><li>b</li></ol></ol>'),
tinyApis.sSetCursor([0, 0, 0], 0),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Bullet list"] > button'),
tinyApis.sAssertContent('<ul><li>a</li><ol style="list-style-type: lower-alpha;"><li>b</li></ol></ul>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 0, 0], 0)
])),
Logger.t('alpha to ul, selection from parent to sublist', GeneralSteps.sequence([
tinyApis.sSetContent('<ol style="list-style-type: lower-alpha;"><li>a</li><ol style="list-style-type: lower-alpha;"><li>b</li></ol></ol>'),
tinyApis.sSetSelection([0, 0, 0], 0, [0, 1, 0, 0], 1),
tinyUi.sClickOnToolbar('click bullist button', 'div[aria-label="Bullet list"] > button'),
tinyApis.sAssertContent('<ul><li>a</li><ul><li>b</li></ul></ul>'),
tinyApis.sAssertSelection([0, 0, 0], 0, [0, 1, 0, 0], 1)
]))
], onSuccess, onFailure);
}, {
indent: false,
plugins: 'lists',
toolbar: 'numlist bullist',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,402 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.IndentTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Indent single LI in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li style="list-style-type: none;">' +
'<ol>' +
'<li>a</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent middle LI in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent single LI in OL and retain OLs list style in the new OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol style="list-style-type: lower-alpha;">' +
'<li>a</li>' +
'<li>b</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol style="list-style-type: lower-alpha;">' +
'<li>a' +
'<ol style="list-style-type: lower-alpha;">' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
});
suite.test('Indent last LI in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:last', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent in table cell in table inside of list should not do anything', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>' +
'<table>' +
'<tr>' +
'<td></td>' +
'</tr>' +
'</table>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'td', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>' +
'<table>' +
'<tr>' +
'<td></td>' +
'</tr>' +
'</table>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'TD');
});
suite.test('Indent last LI to same level as middle LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:last', 1);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent first LI and nested LI OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0, 'li li', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li style="list-style-type: none;">' +
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent second LI to same level as nested LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b' +
'<ul>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent second LI to same level as nested LI 2', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'</ul>' +
'</li>' +
'<li>cd' +
'<ul>' +
'<li>e</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>cd</li>' +
'<li>e</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Indent second and third LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0, 'li:last', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
});
suite.test('Indent second second li with next sibling to nested li', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b' +
'<ul>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'<li>d</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul > li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'<li>d</li>' +
'</ul>'
);
});
suite.test('Indent on second li with inner block element', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li><p>a</p></li>' +
'<li><p>b</p></li>' +
'<li><p>c</p></li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul > li:nth-child(2) > p', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>' +
'<p>a</p>' +
'<ul><li><p>b</p></li></ul>' +
'</li>' +
'<li><p>c</p></li>' +
'</ul>'
);
});
suite.test('Indent already indented last li, ul in ol', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'</ul>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li', 0);
LegacyUnit.execCommand(editor, 'Indent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ul>' +
'<li style="list-style-type: none;">' +
'<ul>' +
'<li>b</li>' +
'</ul>' +
'</li>' +
'</ul>' +
'</li>' +
'</ol>'
);
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom],ol[style|class|data-custom],' +
'ul[style|class|data-custom],dl,dt,dd,em,strong,span,#p,div,br,table,tr,td',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,51 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.IndentTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Remove UL in inline body element contained in LI', function (editor) {
editor.setContent('<ul><li>a</li></ul>');
editor.selection.setCursorLocation();
editor.execCommand('InsertUnorderedList');
LegacyUnit.equal(editor.getContent(), '<p>a</p>');
});
suite.test('Backspace in LI in UL in inline body element contained within LI', function (editor) {
editor.setContent('<ul><li>a</li></ul>');
editor.focus();
editor.selection.select(editor.getBody(), true);
editor.selection.collapse(true);
editor.plugins.lists.backspaceDelete();
LegacyUnit.equal(editor.getContent(), '<p>a</p>');
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
inline: true,
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom],ol[style|class|data-custom],' +
'ul[style|class|data-custom],dl,dt,dd,em,strong,span,#p,div,br',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,90 @@
import { Arbitraries } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { document } from '@ephox/dom-globals';
import { Arr, Option } from '@ephox/katamari';
import { Element } from '@ephox/sugar';
import Jsc from '@ephox/wrap-jsverify';
import { composeList } from '../../../main/ts/listModel/ComposeList';
import { Entry } from '../../../main/ts/listModel/Entry';
import { normalizeEntries } from '../../../main/ts/listModel/NormalizeEntries';
import { parseLists } from '../../../main/ts/listModel/ParseLists';
import { ListType } from 'tinymce/plugins/lists/listModel/Util';
UnitTest.test('tinymce.lists.browser.ListModelTest', () => {
const arbitratyContent = Jsc.bless({
generator: Arbitraries.content('inline').generator.map((el) => [el])
});
const arbitraryEntry = Jsc.record({
isSelected: Jsc.constant(false),
depth: Jsc.integer(1, 10),
content: Jsc.small(arbitratyContent),
listType: Jsc.oneof(Jsc.constant(ListType.OL), Jsc.constant(ListType.UL)),
listAttributes: Jsc.oneof(Jsc.constant({}), Jsc.constant({style: 'list-style-type: lower-alpha;'})),
itemAttributes: Jsc.oneof(Jsc.constant({}), Jsc.constant({style: 'color: red;'})),
});
const arbitraryEntries = Jsc.array(arbitraryEntry);
const composeParseProperty = Jsc.forall(arbitraryEntries, (inputEntries: Entry[]) => {
normalizeEntries(inputEntries);
const outputEntries = composeParse(inputEntries);
return isEqualEntries(inputEntries, outputEntries) || errorMessage(inputEntries, outputEntries);
});
const composeParse = (entries: Entry[]): Entry[] => {
return composeList(document, entries)
.map((list) => parseLists([list], Option.none()))
.bind(Arr.head)
.map((entrySet) => entrySet.entries)
.getOr([]);
};
const isEqualEntries = (a: Entry[], b: Entry[]): boolean => {
return stringifyEntries(a) === stringifyEntries(b);
};
const errorMessage = (inputEntries: Entry[], outputEntries: Entry[]): string => {
return `\nPretty print counterexample:\n` +
`input: [${stringifyEntries(inputEntries)}\n]\n` +
`output: [${stringifyEntries(outputEntries)}\n]`;
};
const stringifyEntries = (entries: Entry[]): string => {
return Arr.map(entries, stringifyEntry).join(',');
};
const stringifyEntry = (entry: Entry): string => {
return `\n {
depth: ${entry.depth}
content: ${entry.content.length > 0 ? serializeElements(entry.content) : '[Empty]'}
listType: ${entry.listType}
isSelected: ${entry.isSelected}
listAttributes: ${JSON.stringify(entry.listAttributes)}
itemAttributes: ${JSON.stringify(entry.itemAttributes)}
}`;
};
const serializeElements = (elms: Element[]): string => {
return Arr.map(elms, (el) => el.dom().outerHTML).join('');
};
Jsc.assert(composeParseProperty, {
size: 500,
tests: 500,
quiet: true
});
// Manual testing. To simplify debugging once a counterexample has been found.
/* const inputEntries: Entry[] = [
{
depth: 2,
content: [Element.fromHtml('<i>stuff</i>')],
listType: ListType.OL,
isSelected: false,
listAttributes: {style: 'list-style-type: lower-alpha;'},
itemAttributes: {}
}
];
throw composeParse(inputEntries); */
});

View File

@@ -0,0 +1,425 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.OutdentTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Outdent inside LI in beginning of OL in LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b' +
'<ol>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent inside LI in middle of OL in LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c' +
'<ol>' +
'<li>d</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent inside LI in end of OL in LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li:last', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
// Nested lists in OL elements
suite.test('Outdent inside LI in beginning of OL in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol li', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b' +
'<ol>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent inside LI in middle of OL in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ol>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c' +
'<ol>' +
'<li>d</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent inside first/last LI in inner OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>1' +
'<ol>' +
'<li>2</li>' +
'<li>3</li>' +
'</ol>' +
'</li>' +
'<li>4</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol li:nth-child(1)', 0, 'ol ol li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>1</li>' +
'<li>2</li>' +
'<li>3</li>' +
'<li>4</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getRng(true).startContainer.nodeValue, '2');
LegacyUnit.equal(editor.selection.getRng(true).endContainer.nodeValue, '3');
});
suite.test('Outdent inside first LI in inner OL where OL is single child of parent LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<li>' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol li:first', 0);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b' +
'<ol>' +
'<li>c</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent inside LI in end of OL in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<ol>' +
'<li>b</li>' +
'<li>c</li>' +
'</ol>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol li:last', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'<li>c</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent inside only child LI in OL in OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol li', 0);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a</li>' +
'<li>b</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'LI');
});
suite.test('Outdent multiple LI in OL and nested OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'</ol>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0, 'li li', 1);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<p>a</p>' +
'<ol>' +
'<li>b</li>' +
'</ol>'
);
});
suite.test('Outdent on li with inner block element', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li><p>a</p></li>' +
'<li><p>b</p></li>' +
'<li><p>c</p></li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li:nth-child(2) p', 0);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li><p>a</p></li>' +
'</ul>' +
'<p>b</p>' +
'<ul>' +
'<li><p>c</p></li>' +
'</ul>'
);
});
suite.test('Outdent on nested li with inner block element', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>' +
'<p>a</p>' +
'<ul><li><p>b</p></li></ul>' +
'</li>' +
'<li><p>c</p></li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li:nth-child(1) li p', 0);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li><p>a</p></li>' +
'<li><p>b</p></li>' +
'<li><p>c</p></li>' +
'</ul>'
);
});
suite.test('Outdent nested ul in ol', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li style="list-style-type: none;">' +
'<ul>' +
'<li>a</li>' +
'</ul>' +
'</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul', 0);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>'
);
});
suite.test('Outdenting an item should not affect its attributes', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li style="color: red;" class="xyz">a' +
'<ul>' +
'<li style="color: blue;">b</li>' +
'</ul>' +
'</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul ul li', 0);
LegacyUnit.execCommand(editor, 'Outdent');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li style="color: red;" class="xyz">a</li>' +
'<li style="color: blue;">b</li>' +
'</ul>'
);
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom],ol[style|class|data-custom],' +
'ul[style|class|data-custom],dl,dt,dd,em,strong,span,#p,div,br',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,505 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Env from 'tinymce/core/api/Env';
import Plugin from 'tinymce/plugins/lists/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.RemoveTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('Remove UL at single LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<p>a</p>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove UL at start LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<p>a</p>' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove UL at start empty LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li><br></li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<p>\u00a0</p>' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Remove UL at middle LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<p>b</p>' +
'<ul>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove UL at middle empty LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li><br></li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:nth-child(2)', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<p>\u00a0</p>' +
'<ul>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Remove UL at end LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:last', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>' +
'<p>c</p>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove UL at end empty LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'<li><br></li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:last', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>' +
'<p>\u00a0</p>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Remove UL at middle LI inside parent OL', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a</li>' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'<li>e</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'</ul>' +
'</li>' +
'</ol>' +
'<p>c</p>' +
'<ol>' +
'<li style="list-style-type: none;">' +
'<ul>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'<li>e</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove UL at middle LI inside parent OL (html5)', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'<li>e</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ul li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'</ul>' +
'</li>' +
'</ol>' +
'<p>c</p>' +
'<ol>' +
'<li style="list-style-type: none;">' +
'<ul>' +
'<li>d</li>' +
'</ul>' +
'</li>' +
'<li>e</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove OL on a deep nested LI', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'<li>c' +
'<ol>' +
'<li>d</li>' +
'<li>e</li>' +
'<li>f</li>' +
'</ol>' +
'</li>' +
'<li>g</li>' +
'<li>h</li>' +
'</ol>' +
'</li>' +
'<li>i</li>' +
'</ol>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'ol ol ol li:nth-child(2)', 1);
LegacyUnit.execCommand(editor, 'InsertOrderedList');
LegacyUnit.equal(editor.getContent(),
'<ol>' +
'<li>a' +
'<ol>' +
'<li>b</li>' +
'<li>c' +
'<ol>' +
'<li>d</li>' +
'</ol>' +
'</li>' +
'</ol>' +
'</li>' +
'</ol>' +
'<p>e</p>' +
'<ol>' +
'<li style="list-style-type: none;">' +
'<ol>' +
'<li style="list-style-type: none;">' +
'<ol>' +
'<li>f</li>' +
'</ol>' +
'</li>' +
'<li>g</li>' +
'<li>h</li>' +
'</ol>' +
'</li>' +
'<li>i</li>' +
'</ol>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'P');
});
suite.test('Remove UL with single LI in BR mode', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'a'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'BODY');
editor.settings.forced_root_block = 'p';
});
suite.test('Remove UL with multiple LI in BR mode', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li>b</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:first', 1, 'li:last', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'a<br />' +
'b'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'BODY');
editor.settings.forced_root_block = 'p';
});
suite.test('Remove empty UL between two textblocks', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<div>a</div>' +
'<ul>' +
'<li></li>' +
'</ul>' +
'<div>b</div>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:first', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<div>a</div>' +
'<p>\u00a0</p>' +
'<div>b</div>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Remove indented list with single item', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'</ul>' +
'</li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li', 0, 'li li', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<p>b</p>' +
'<ul>' +
'<li>c</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getNode().nodeName, 'P');
});
suite.test('Remove indented list with multiple items', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a' +
'<ul>' +
'<li>b</li>' +
'<li>c</li>' +
'</ul>' +
'</li>' +
'<li>d</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li li:first', 0, 'li li:last', 1);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<p>b</p>' +
'<p>c</p>' +
'<ul>' +
'<li>d</li>' +
'</ul>'
);
LegacyUnit.equal(editor.selection.getStart().firstChild.data, 'b');
LegacyUnit.equal(editor.selection.getEnd().firstChild.data, 'c');
});
suite.test('Remove indented list with multiple items', function (editor) {
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<ul>' +
'<li>a</li>' +
'<li><p>b</p></li>' +
'<li>c</li>' +
'</ul>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'p', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<ul>' +
'<li>a</li>' +
'</ul>' +
'<p>b</p>' +
'<ul>' +
'<li>c</li>' +
'</ul>'
);
});
// Ignore on IE 7, 8 this is a known bug not worth fixing
if (!Env.ie || Env.ie > 8) {
suite.test('Remove empty UL between two textblocks in BR mode', function (editor) {
editor.settings.forced_root_block = false;
editor.getBody().innerHTML = LegacyUnit.trimBrs(
'<div>a</div>' +
'<ul>' +
'<li></li>' +
'</ul>' +
'<div>b</div>'
);
editor.focus();
LegacyUnit.setSelection(editor, 'li:first', 0);
LegacyUnit.execCommand(editor, 'InsertUnorderedList');
LegacyUnit.equal(editor.getContent(),
'<div>a</div>' +
'<br />' +
'<div>b</div>'
);
LegacyUnit.equal(editor.selection.getStart().nodeName, 'BR');
editor.settings.forced_root_block = 'p';
});
}
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
plugins: 'lists',
add_unload_trigger: false,
disable_nodechange: true,
indent: false,
entities: 'raw',
valid_elements:
'li[style|class|data-custom],ol[style|class|data-custom],' +
'ul[style|class|data-custom],dl,dt,dd,em,strong,span,#p,div,br',
valid_styles: {
'*': 'color,font-size,font-family,background-color,font-weight,' +
'font-style,text-decoration,float,margin,margin-top,margin-right,' +
'margin-bottom,margin-left,display,position,top,left,list-style-type'
},
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,66 @@
import { GeneralSteps, Logger, Pipeline, Step, UiFinder } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyDom, TinyLoader, TinyUi } from '@ephox/mcagar';
import ListsPlugin from 'tinymce/plugins/lists/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('browser.tinymce.plugins.lists.TableInListTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
ModernTheme();
ListsPlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const tinyUi = TinyUi(editor);
Pipeline.async({}, [
Logger.t('unlist table in list then add list inside table', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li><table><tbody><tr><td>a</td><td>b</td></tr></tbody></table></li></ul>'),
tinyApis.sSetCursor([0, 0, 0, 0, 0, 0, 0], 0),
tinyUi.sClickOnToolbar('click list button', 'div[aria-label="Bullet list"] button'),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><ul><li>a</li></ul></td><td>b</td></tr></tbody></table></li></ul>'),
tinyUi.sClickOnToolbar('click list button', 'div[aria-label="Bullet list"] button'),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><p>a</p></td><td>b</td></tr></tbody></table></li></ul>')
])),
Logger.t('delete list in table test', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li><table><tbody><tr><td><ul><li><p>a</p></li></ul></td><td><p>b</p></td></tr></tbody></table></li></ul>'),
tinyApis.sSetSelection([0, 0, 0, 0, 0, 0, 0, 0, 0], 0, [0, 0, 0, 0, 0, 0, 0, 0, 0], 1),
Step.sync(function () {
editor.plugins.lists.backspaceDelete();
editor.plugins.lists.backspaceDelete();
}),
tinyApis.sAssertSelection([0, 0, 0, 0, 0, 0, 0], 0, [0, 0, 0, 0, 0, 0, 0], 0),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><p>&nbsp;</p></td><td><p>b</p></td></tr></tbody></table></li></ul>')
])),
Logger.t('focus on table cell in list does not activate button', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li><table><tbody><tr><td>a</td><td>b</td></tr></tbody></table></li></ul>'),
tinyApis.sSetCursor([0, 0, 0, 0, 0, 0, 0], 0),
UiFinder.sNotExists(TinyDom.fromDom(editor.getContainer()), 'div[aria-label="Bullet list"][aria-pressed="true"]')
])),
Logger.t('indent and outdent li in ul in list in table in list', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li><table><tbody><tr><td><ul><li><p>a</p></li><li><p>b</p></li></ul></td><td><p>b</p></td></tr></tbody></table></li></ul>'),
tinyApis.sSetSelection([0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 0, [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 1),
tinyUi.sClickOnToolbar('click increase indent', 'div[aria-label="Increase indent"] button'),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><ul><li><p>a</p><ul><li><p>b</p></li></ul></li></ul></td><td><p>b</p></td></tr></tbody></table></li></ul>'),
tinyUi.sClickOnToolbar('click decrease indent', 'div[aria-label="Decrease indent"] button'),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><ul><li><p>a</p></li><li><p>b</p></li></ul></td><td><p>b</p></td></tr></tbody></table></li></ul>'),
tinyUi.sClickOnToolbar('click decrease indent', 'div[aria-label="Decrease indent"] button'),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><ul><li><p>a</p></li></ul><p>b</p></td><td><p>b</p></td></tr></tbody></table></li></ul>')
])),
Logger.t('toggle from UL to OL in list in table in list only changes inner list', GeneralSteps.sequence([
tinyApis.sSetContent('<ul><li><table><tbody><tr><td><ul><li><p>a</p></li><li><p>b</p></li></ul></td><td><p>b</p></td></tr></tbody></table></li></ul>'),
tinyApis.sSetSelection([0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 0, [0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 1),
tinyUi.sClickOnToolbar('click numlist button', 'div[aria-label="Numbered list"] button'),
tinyApis.sAssertContent('<ul><li><table><tbody><tr><td><ol><li><p>a</p></li><li><p>b</p></li></ol></td><td><p>b</p></td></tr></tbody></table></li></ul>')
]))
], onSuccess, onFailure);
}, {
plugins: 'lists',
toolbar: 'bullist numlist indent outdent',
indent: false,
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,31 @@
import { GeneralSteps, Logger, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
import ListsPlugin from 'tinymce/plugins/lists/Plugin';
import ModernTheme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('tinymce.lists.browser.ToggleListWithEmptyLiTest', (success, failure) => {
ModernTheme();
ListsPlugin();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyApis = TinyApis(editor);
const tinyUi = TinyUi(editor);
Pipeline.async({}, [
Logger.t('toggle bullet list on list with two empty LIs', GeneralSteps.sequence([
tinyApis.sFocus,
tinyApis.sSetContent('<ul><li>a</li><li>&nbsp;</li><li>&nbsp;</li><li>b</li></ul>'),
tinyApis.sSetSelection([0, 0, 0], 0, [0, 3, 0], 1),
tinyUi.sClickOnToolbar('click list', 'div[aria-label="Bullet list"] > button'),
tinyApis.sAssertContent('<p>a</p><p>&nbsp;</p><p>&nbsp;</p><p>b</p>')
])),
], onSuccess, onFailure);
}, {
indent: false,
plugins: 'lists',
toolbar: '',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});