From 50ec8b6c4e3c949dac466ac4523025da721d840c Mon Sep 17 00:00:00 2001
From: baka [cE=false] [cE=true][cE=false]abc[cE=false][cE=false][cE=false][cE=false] Nam nisi elit, cursus in rhoncus sit amet, pulvinar laoreet leo. Nam sed lectus quam, ut sagittis tellus. Quisque dignissim mauris a augue rutrum tempor. Donec vitae purus nec massa vestibulum ornare sit ame[cE=false]t id tellus. Nunc quam mauris, fermentum nec lacinia eget, sollicitudin nec ante. Aliquam molestie volutpat dapibus. Nunc interdum viverra sodales. Morbi laoreet pulvinar gravida. Quisque ut turpis sagittis nunc accumsan vehicula. Duis elementum congue ultrices. Cras faucibus feugiat arcu quis lacinia. In hac habitasse platea dictumst. Pellentesque fermentum magna sit amet tellus varius ullamcorper. Vestibulum at urna augue, eget varius neque. Fusce facilisis venenatis dapibus. Integer non sem at arcu euismod tempor nec sed nisl. Morbi ultricies, mauris ut ultricies adipiscing, felis odio condimentum massa, et luctus est nunc nec eros. [cE=false[cE=true]] [cE=false] [cE=false]
+
+ Raw html
+ Bolt Bolt \u00a0
");
+ if (imageData != null) {
+ editor.dom.replace(editor.dom.createFragment(img_div + "
"), imageData.parent);
+ }
+ else {
+ editor.insertContent(img_div + "
");
+ }
}
}
});
+ if (imageData != null) {
+ win
+ .find("#file")
+ .value(imageData.file)
+ .fire("change");
+ win
+ .find("#alternate")
+ .value(imageData.alt)
+ .fire("change");
+ win
+ .find("#width")
+ .value(imageData.width)
+ .fire("change");
+ win
+ .find("#height")
+ .value(imageData.height)
+ .fire("change");
+ }
}
return {
open: open
@@ -298,7 +341,7 @@ exports["default"] = default_1;
exports.__esModule = true;
var plugin_1 = __webpack_require__(0);
-tinymce.PluginManager.add("trunk-images", plugin_1["default"]);
+tinymce.PluginManager.add("ccm-cms-images", plugin_1["default"]);
/***/ })
diff --git a/ccm-core/web/assets/tinymce/js/tinymce/plugins/ccm-cms-images/plugin.min.js b/ccm-core/web/assets/tinymce/js/tinymce/plugins/ccm-cms-images/plugin.min.js
new file mode 100644
index 000000000..487e53b1c
--- /dev/null
+++ b/ccm-core/web/assets/tinymce/js/tinymce/plugins/ccm-cms-images/plugin.min.js
@@ -0,0 +1 @@
+!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=2)}([function(e,t,n){"use strict";t.__esModule=!0;var i=n(1),a=function(e,t){e.addButton("ccm-cms-images-button",{icon:"image",tooltip:"Insert/Edit image",onlick:i.default(e).open,stateSelector:"div.image"}),e.addMenuItem("ccm-cms-images-menu",{icon:"image",text:"Insert Images",onclick:i.default(e).open,stateSelector:"image",context:"insert",prependToContext:!0})};t.default=a},function(e,t,n){"use strict";function i(e){function t(e){var t=e.selection.getNode(),n=e.dom.getParent(t,"div.image"),i=e.dom.select("img",n)[0];if(null!=n){return{file:i.getAttribute("src"),width:i.getAttribute("width").slice(0,-2),height:i.getAttribute("height").slice(0,-2),alt:i.getAttribute("alt"),parent:n}}return null}function n(){var n=t(e),i="",a=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),l=new tinymce.ui.TextBox({name:"file",label:"File:",disabled:!0});a.add(l);var r=new tinymce.ui.Button({name:"browse_images",text:"Browse Images",onclick:function(){var e=window.location.href,t=e.lastIndexOf("/"),n=e.slice(0,t+1)+"image_select.jsp";window.open(n,"_blank","scrollbars=yes,directories=no,toolbar=no,width=800,height=600,status=no,menubar=no");window.openCCM=new Object,window.openCCM.imageSet=function(e){return l.text(e.src),p.find("#file").value(e.src).fire("change"),p.find("#width").value(e.width).fire("change"),p.find("#height").value(e.height).fire("change"),i=e.name,!0}}});a.add(r);var o=new tinymce.ui.TextBox({name:"alternate",label:"Alternate:"}),c=new tinymce.ui.TextBox({name:"title",label:"Title:"}),u=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),d=new tinymce.ui.Label({text:"Alignment:"}),m=new tinymce.ui.ListBox({name:"alignment",values:[{text:"Not set",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]});u.add(d),u.add(m);var s=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),f=new tinymce.ui.Label({text:"Fancy Box:"}),g=new tinymce.ui.ListBox({name:"fancybox",values:[{text:"None",value:""},{text:"Zoom",value:"imageZoom"},{text:"Gallery",value:"imageGallery"}]});s.add(f),s.add(g);var h=new tinymce.ui.Checkbox({label:"Caption:",name:"caption"}),w=new tinymce.ui.Container({label:"Dimension",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),x=new tinymce.ui.TextBox({name:"width",label:"Width"}),v=new tinymce.ui.TextBox({name:"height",label:"Height"});w.add(x),w.add({type:"label",text:"X"}),w.add(v);var p=e.windowManager.open({title:"Insert/Modify Image",width:800,height:600,body:[a,o,c,u,s,h,w],onsubmit:function(){var t=p.find("#file").value(),a=p.find("#alternate").value(),l=p.find("#width").value(),r=p.find("#height").value(),o=p.find("#title").value(),c=p.find("#alignment").value(),u=p.find("#fancybox").value();if(null!=t){var d="',m=" '+d+"",s="";p.find("#caption").value()&&(s=''+i+"");var f='
"),n.parent):e.insertContent(f+"
")}}});null!=n&&(p.find("#file").value(n.file).fire("change"),p.find("#alternate").value(n.alt).fire("change"),p.find("#width").value(n.width).fire("change"),p.find("#height").value(n.height).fire("change"))}return{open:n}}t.__esModule=!0,t.default=i},function(e,t,n){"use strict";t.__esModule=!0;var i=n(0);tinymce.PluginManager.add("ccm-cms-images",i.default)}]);
\ No newline at end of file
diff --git a/ccm-core/web/assets/tinymce/js/tinymce/plugins/image/plugin.js b/ccm-core/web/assets/tinymce/js/tinymce/plugins/image/plugin.js
new file mode 100644
index 000000000..179bf650c
--- /dev/null
+++ b/ccm-core/web/assets/tinymce/js/tinymce/plugins/image/plugin.js
@@ -0,0 +1,1211 @@
+(function () {
+var image = (function () {
+ 'use strict';
+
+ var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
+
+ var hasDimensions = function (editor) {
+ return editor.settings.image_dimensions === false ? false : true;
+ };
+ var hasAdvTab = function (editor) {
+ return editor.settings.image_advtab === true ? true : false;
+ };
+ var getPrependUrl = function (editor) {
+ return editor.getParam('image_prepend_url', '');
+ };
+ var getClassList = function (editor) {
+ return editor.getParam('image_class_list');
+ };
+ var hasDescription = function (editor) {
+ return editor.settings.image_description === false ? false : true;
+ };
+ var hasImageTitle = function (editor) {
+ return editor.settings.image_title === true ? true : false;
+ };
+ var hasImageCaption = function (editor) {
+ return editor.settings.image_caption === true ? true : false;
+ };
+ var getImageList = function (editor) {
+ return editor.getParam('image_list', false);
+ };
+ var hasUploadUrl = function (editor) {
+ return editor.getParam('images_upload_url', false);
+ };
+ var hasUploadHandler = function (editor) {
+ return editor.getParam('images_upload_handler', false);
+ };
+ var getUploadUrl = function (editor) {
+ return editor.getParam('images_upload_url');
+ };
+ var getUploadHandler = function (editor) {
+ return editor.getParam('images_upload_handler');
+ };
+ var getUploadBasePath = function (editor) {
+ return editor.getParam('images_upload_base_path');
+ };
+ var getUploadCredentials = function (editor) {
+ return editor.getParam('images_upload_credentials');
+ };
+ var $_bbugoqctjm7ol69d = {
+ hasDimensions: hasDimensions,
+ hasAdvTab: hasAdvTab,
+ getPrependUrl: getPrependUrl,
+ getClassList: getClassList,
+ hasDescription: hasDescription,
+ hasImageTitle: hasImageTitle,
+ hasImageCaption: hasImageCaption,
+ getImageList: getImageList,
+ hasUploadUrl: hasUploadUrl,
+ hasUploadHandler: hasUploadHandler,
+ getUploadUrl: getUploadUrl,
+ getUploadHandler: getUploadHandler,
+ getUploadBasePath: getUploadBasePath,
+ getUploadCredentials: getUploadCredentials
+ };
+
+ var Global = typeof window !== 'undefined' ? window : Function('return this;')();
+
+ var path = function (parts, scope) {
+ var o = scope !== undefined && scope !== null ? scope : Global;
+ for (var i = 0; i < parts.length && o !== undefined && o !== null; ++i)
+ o = o[parts[i]];
+ return o;
+ };
+ var resolve = function (p, scope) {
+ var parts = p.split('.');
+ return path(parts, scope);
+ };
+
+ var unsafe = function (name, scope) {
+ return resolve(name, scope);
+ };
+ var getOrDie = function (name, scope) {
+ var actual = unsafe(name, scope);
+ if (actual === undefined || actual === null)
+ throw name + ' not available on this browser';
+ return actual;
+ };
+ var $_g9oowjcwjm7ol69l = { getOrDie: getOrDie };
+
+ function FileReader () {
+ var f = $_g9oowjcwjm7ol69l.getOrDie('FileReader');
+ return new f();
+ }
+
+ var global$1 = tinymce.util.Tools.resolve('tinymce.util.Promise');
+
+ var global$2 = tinymce.util.Tools.resolve('tinymce.util.Tools');
+
+ var global$3 = tinymce.util.Tools.resolve('tinymce.util.XHR');
+
+ var parseIntAndGetMax = function (val1, val2) {
+ return Math.max(parseInt(val1, 10), parseInt(val2, 10));
+ };
+ var getImageSize = function (url, callback) {
+ var img = document.createElement('img');
+ function done(width, height) {
+ if (img.parentNode) {
+ img.parentNode.removeChild(img);
+ }
+ callback({
+ width: width,
+ height: height
+ });
+ }
+ img.onload = function () {
+ var width = parseIntAndGetMax(img.width, img.clientWidth);
+ var height = parseIntAndGetMax(img.height, img.clientHeight);
+ done(width, height);
+ };
+ img.onerror = function () {
+ done(0, 0);
+ };
+ var style = img.style;
+ style.visibility = 'hidden';
+ style.position = 'fixed';
+ style.bottom = style.left = '0px';
+ style.width = style.height = 'auto';
+ document.body.appendChild(img);
+ img.src = url;
+ };
+ var buildListItems = function (inputList, itemCallback, startItems) {
+ function appendItems(values, output) {
+ output = output || [];
+ global$2.each(values, function (item) {
+ var menuItem = { text: item.text || item.title };
+ if (item.menu) {
+ menuItem.menu = appendItems(item.menu);
+ } else {
+ menuItem.value = item.value;
+ itemCallback(menuItem);
+ }
+ output.push(menuItem);
+ });
+ return output;
+ }
+ return appendItems(inputList, startItems || []);
+ };
+ var removePixelSuffix = function (value) {
+ if (value) {
+ value = value.replace(/px$/, '');
+ }
+ return value;
+ };
+ var addPixelSuffix = function (value) {
+ if (value.length > 0 && /^[0-9]+$/.test(value)) {
+ value += 'px';
+ }
+ return value;
+ };
+ var mergeMargins = function (css) {
+ if (css.margin) {
+ var splitMargin = css.margin.split(' ');
+ switch (splitMargin.length) {
+ case 1:
+ css['margin-top'] = css['margin-top'] || splitMargin[0];
+ css['margin-right'] = css['margin-right'] || splitMargin[0];
+ css['margin-bottom'] = css['margin-bottom'] || splitMargin[0];
+ css['margin-left'] = css['margin-left'] || splitMargin[0];
+ break;
+ case 2:
+ css['margin-top'] = css['margin-top'] || splitMargin[0];
+ css['margin-right'] = css['margin-right'] || splitMargin[1];
+ css['margin-bottom'] = css['margin-bottom'] || splitMargin[0];
+ css['margin-left'] = css['margin-left'] || splitMargin[1];
+ break;
+ case 3:
+ css['margin-top'] = css['margin-top'] || splitMargin[0];
+ css['margin-right'] = css['margin-right'] || splitMargin[1];
+ css['margin-bottom'] = css['margin-bottom'] || splitMargin[2];
+ css['margin-left'] = css['margin-left'] || splitMargin[1];
+ break;
+ case 4:
+ css['margin-top'] = css['margin-top'] || splitMargin[0];
+ css['margin-right'] = css['margin-right'] || splitMargin[1];
+ css['margin-bottom'] = css['margin-bottom'] || splitMargin[2];
+ css['margin-left'] = css['margin-left'] || splitMargin[3];
+ }
+ delete css.margin;
+ }
+ return css;
+ };
+ var createImageList = function (editor, callback) {
+ var imageList = $_bbugoqctjm7ol69d.getImageList(editor);
+ if (typeof imageList === 'string') {
+ global$3.send({
+ url: imageList,
+ success: function (text) {
+ callback(JSON.parse(text));
+ }
+ });
+ } else if (typeof imageList === 'function') {
+ imageList(callback);
+ } else {
+ callback(imageList);
+ }
+ };
+ var waitLoadImage = function (editor, data, imgElm) {
+ function selectImage() {
+ imgElm.onload = imgElm.onerror = null;
+ if (editor.selection) {
+ editor.selection.select(imgElm);
+ editor.nodeChanged();
+ }
+ }
+ imgElm.onload = function () {
+ if (!data.width && !data.height && $_bbugoqctjm7ol69d.hasDimensions(editor)) {
+ editor.dom.setAttribs(imgElm, {
+ width: imgElm.clientWidth,
+ height: imgElm.clientHeight
+ });
+ }
+ selectImage();
+ };
+ imgElm.onerror = selectImage;
+ };
+ var blobToDataUri = function (blob) {
+ return new global$1(function (resolve, reject) {
+ var reader = new FileReader();
+ reader.onload = function () {
+ resolve(reader.result);
+ };
+ reader.onerror = function () {
+ reject(FileReader.error.message);
+ };
+ reader.readAsDataURL(blob);
+ });
+ };
+ var $_b0dhuucujm7ol69f = {
+ getImageSize: getImageSize,
+ buildListItems: buildListItems,
+ removePixelSuffix: removePixelSuffix,
+ addPixelSuffix: addPixelSuffix,
+ mergeMargins: mergeMargins,
+ createImageList: createImageList,
+ waitLoadImage: waitLoadImage,
+ blobToDataUri: blobToDataUri
+ };
+
+ var global$4 = tinymce.util.Tools.resolve('tinymce.dom.DOMUtils');
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ var shallow = function (old, nu) {
+ return nu;
+ };
+ var baseMerge = function (merger) {
+ return function () {
+ var objects = new Array(arguments.length);
+ for (var i = 0; i < objects.length; i++)
+ objects[i] = arguments[i];
+ if (objects.length === 0)
+ throw new Error('Can\'t merge zero objects');
+ var ret = {};
+ for (var j = 0; j < objects.length; j++) {
+ var curObject = objects[j];
+ for (var key in curObject)
+ if (hasOwnProperty.call(curObject, key)) {
+ ret[key] = merger(ret[key], curObject[key]);
+ }
+ }
+ return ret;
+ };
+ };
+
+ var merge = baseMerge(shallow);
+
+ var DOM = global$4.DOM;
+ var getHspace = function (image) {
+ if (image.style.marginLeft && image.style.marginRight && image.style.marginLeft === image.style.marginRight) {
+ return $_b0dhuucujm7ol69f.removePixelSuffix(image.style.marginLeft);
+ } else {
+ return '';
+ }
+ };
+ var getVspace = function (image) {
+ if (image.style.marginTop && image.style.marginBottom && image.style.marginTop === image.style.marginBottom) {
+ return $_b0dhuucujm7ol69f.removePixelSuffix(image.style.marginTop);
+ } else {
+ return '';
+ }
+ };
+ var getBorder = function (image) {
+ if (image.style.borderWidth) {
+ return $_b0dhuucujm7ol69f.removePixelSuffix(image.style.borderWidth);
+ } else {
+ return '';
+ }
+ };
+ var getAttrib = function (image, name$$1) {
+ if (image.hasAttribute(name$$1)) {
+ return image.getAttribute(name$$1);
+ } else {
+ return '';
+ }
+ };
+ var getStyle = function (image, name$$1) {
+ return image.style[name$$1] ? image.style[name$$1] : '';
+ };
+ var hasCaption = function (image) {
+ return image.parentNode !== null && image.parentNode.nodeName === 'FIGURE';
+ };
+ var setAttrib = function (image, name$$1, value) {
+ image.setAttribute(name$$1, value);
+ };
+ var wrapInFigure = function (image) {
+ var figureElm = DOM.create('figure', { class: 'image' });
+ DOM.insertAfter(figureElm, image);
+ figureElm.appendChild(image);
+ figureElm.appendChild(DOM.create('figcaption', { contentEditable: true }, 'Caption'));
+ figureElm.contentEditable = 'false';
+ };
+ var removeFigure = function (image) {
+ var figureElm = image.parentNode;
+ DOM.insertAfter(image, figureElm);
+ DOM.remove(figureElm);
+ };
+ var toggleCaption = function (image) {
+ if (hasCaption(image)) {
+ removeFigure(image);
+ } else {
+ wrapInFigure(image);
+ }
+ };
+ var normalizeStyle = function (image, normalizeCss) {
+ var attrValue = image.getAttribute('style');
+ var value = normalizeCss(attrValue !== null ? attrValue : '');
+ if (value.length > 0) {
+ image.setAttribute('style', value);
+ image.setAttribute('data-mce-style', value);
+ } else {
+ image.removeAttribute('style');
+ }
+ };
+ var setSize = function (name$$1, normalizeCss) {
+ return function (image, name$$1, value) {
+ if (image.style[name$$1]) {
+ image.style[name$$1] = $_b0dhuucujm7ol69f.addPixelSuffix(value);
+ normalizeStyle(image, normalizeCss);
+ } else {
+ setAttrib(image, name$$1, value);
+ }
+ };
+ };
+ var getSize = function (image, name$$1) {
+ if (image.style[name$$1]) {
+ return $_b0dhuucujm7ol69f.removePixelSuffix(image.style[name$$1]);
+ } else {
+ return getAttrib(image, name$$1);
+ }
+ };
+ var setHspace = function (image, value) {
+ var pxValue = $_b0dhuucujm7ol69f.addPixelSuffix(value);
+ image.style.marginLeft = pxValue;
+ image.style.marginRight = pxValue;
+ };
+ var setVspace = function (image, value) {
+ var pxValue = $_b0dhuucujm7ol69f.addPixelSuffix(value);
+ image.style.marginTop = pxValue;
+ image.style.marginBottom = pxValue;
+ };
+ var setBorder = function (image, value) {
+ var pxValue = $_b0dhuucujm7ol69f.addPixelSuffix(value);
+ image.style.borderWidth = pxValue;
+ };
+ var setBorderStyle = function (image, value) {
+ image.style.borderStyle = value;
+ };
+ var getBorderStyle = function (image) {
+ return getStyle(image, 'borderStyle');
+ };
+ var isFigure = function (elm) {
+ return elm.nodeName === 'FIGURE';
+ };
+ var defaultData = function () {
+ return {
+ src: '',
+ alt: '',
+ title: '',
+ width: '',
+ height: '',
+ class: '',
+ style: '',
+ caption: false,
+ hspace: '',
+ vspace: '',
+ border: '',
+ borderStyle: ''
+ };
+ };
+ var getStyleValue = function (normalizeCss, data) {
+ var image = document.createElement('img');
+ setAttrib(image, 'style', data.style);
+ if (getHspace(image) || data.hspace !== '') {
+ setHspace(image, data.hspace);
+ }
+ if (getVspace(image) || data.vspace !== '') {
+ setVspace(image, data.vspace);
+ }
+ if (getBorder(image) || data.border !== '') {
+ setBorder(image, data.border);
+ }
+ if (getBorderStyle(image) || data.borderStyle !== '') {
+ setBorderStyle(image, data.borderStyle);
+ }
+ return normalizeCss(image.getAttribute('style'));
+ };
+ var create = function (normalizeCss, data) {
+ var image = document.createElement('img');
+ write(normalizeCss, merge(data, { caption: false }), image);
+ setAttrib(image, 'alt', data.alt);
+ if (data.caption) {
+ var figure = DOM.create('figure', { class: 'image' });
+ figure.appendChild(image);
+ figure.appendChild(DOM.create('figcaption', { contentEditable: true }, 'Caption'));
+ figure.contentEditable = 'false';
+ return figure;
+ } else {
+ return image;
+ }
+ };
+ var read = function (normalizeCss, image) {
+ return {
+ src: getAttrib(image, 'src'),
+ alt: getAttrib(image, 'alt'),
+ title: getAttrib(image, 'title'),
+ width: getSize(image, 'width'),
+ height: getSize(image, 'height'),
+ class: getAttrib(image, 'class'),
+ style: normalizeCss(getAttrib(image, 'style')),
+ caption: hasCaption(image),
+ hspace: getHspace(image),
+ vspace: getVspace(image),
+ border: getBorder(image),
+ borderStyle: getStyle(image, 'borderStyle')
+ };
+ };
+ var updateProp = function (image, oldData, newData, name$$1, set) {
+ if (newData[name$$1] !== oldData[name$$1]) {
+ set(image, name$$1, newData[name$$1]);
+ }
+ };
+ var normalized = function (set, normalizeCss) {
+ return function (image, name$$1, value) {
+ set(image, value);
+ normalizeStyle(image, normalizeCss);
+ };
+ };
+ var write = function (normalizeCss, newData, image) {
+ var oldData = read(normalizeCss, image);
+ updateProp(image, oldData, newData, 'caption', function (image, _name, _value) {
+ return toggleCaption(image);
+ });
+ updateProp(image, oldData, newData, 'src', setAttrib);
+ updateProp(image, oldData, newData, 'alt', setAttrib);
+ updateProp(image, oldData, newData, 'title', setAttrib);
+ updateProp(image, oldData, newData, 'width', setSize('width', normalizeCss));
+ updateProp(image, oldData, newData, 'height', setSize('height', normalizeCss));
+ updateProp(image, oldData, newData, 'class', setAttrib);
+ updateProp(image, oldData, newData, 'style', normalized(function (image, value) {
+ return setAttrib(image, 'style', value);
+ }, normalizeCss));
+ updateProp(image, oldData, newData, 'hspace', normalized(setHspace, normalizeCss));
+ updateProp(image, oldData, newData, 'vspace', normalized(setVspace, normalizeCss));
+ updateProp(image, oldData, newData, 'border', normalized(setBorder, normalizeCss));
+ updateProp(image, oldData, newData, 'borderStyle', normalized(setBorderStyle, normalizeCss));
+ };
+
+ var normalizeCss = function (editor, cssText) {
+ var css = editor.dom.styles.parse(cssText);
+ var mergedCss = $_b0dhuucujm7ol69f.mergeMargins(css);
+ var compressed = editor.dom.styles.parse(editor.dom.styles.serialize(mergedCss));
+ return editor.dom.styles.serialize(compressed);
+ };
+ var getSelectedImage = function (editor) {
+ var imgElm = editor.selection.getNode();
+ var figureElm = editor.dom.getParent(imgElm, 'figure.image');
+ if (figureElm) {
+ return editor.dom.select('img', figureElm)[0];
+ }
+ if (imgElm && (imgElm.nodeName !== 'IMG' || imgElm.getAttribute('data-mce-object') || imgElm.getAttribute('data-mce-placeholder'))) {
+ return null;
+ }
+ return imgElm;
+ };
+ var splitTextBlock = function (editor, figure) {
+ var dom = editor.dom;
+ var textBlock = dom.getParent(figure.parentNode, function (node) {
+ return editor.schema.getTextBlockElements()[node.nodeName];
+ });
+ if (textBlock) {
+ return dom.split(textBlock, figure);
+ } else {
+ return figure;
+ }
+ };
+ var readImageDataFromSelection = function (editor) {
+ var image = getSelectedImage(editor);
+ return image ? read(function (css) {
+ return normalizeCss(editor, css);
+ }, image) : defaultData();
+ };
+ var insertImageAtCaret = function (editor, data) {
+ var elm = create(function (css) {
+ return normalizeCss(editor, css);
+ }, data);
+ editor.dom.setAttrib(elm, 'data-mce-id', '__mcenew');
+ editor.focus();
+ editor.selection.setContent(elm.outerHTML);
+ var insertedElm = editor.dom.select('*[data-mce-id="__mcenew"]')[0];
+ editor.dom.setAttrib(insertedElm, 'data-mce-id', null);
+ if (isFigure(insertedElm)) {
+ var figure = splitTextBlock(editor, insertedElm);
+ editor.selection.select(figure);
+ } else {
+ editor.selection.select(insertedElm);
+ }
+ };
+ var syncSrcAttr = function (editor, image) {
+ editor.dom.setAttrib(image, 'src', image.getAttribute('src'));
+ };
+ var deleteImage = function (editor, image) {
+ if (image) {
+ var elm = editor.dom.is(image.parentNode, 'figure.image') ? image.parentNode : image;
+ editor.dom.remove(elm);
+ editor.focus();
+ editor.nodeChanged();
+ if (editor.dom.isEmpty(editor.getBody())) {
+ editor.setContent('');
+ editor.selection.setCursorLocation();
+ }
+ }
+ };
+ var writeImageDataToSelection = function (editor, data) {
+ var image = getSelectedImage(editor);
+ write(function (css) {
+ return normalizeCss(editor, css);
+ }, data, image);
+ syncSrcAttr(editor, image);
+ if (isFigure(image.parentNode)) {
+ var figure = image.parentNode;
+ splitTextBlock(editor, figure);
+ editor.selection.select(image.parentNode);
+ } else {
+ editor.selection.select(image);
+ $_b0dhuucujm7ol69f.waitLoadImage(editor, data, image);
+ }
+ };
+ var insertOrUpdateImage = function (editor, data) {
+ var image = getSelectedImage(editor);
+ if (image) {
+ if (data.src) {
+ writeImageDataToSelection(editor, data);
+ } else {
+ deleteImage(editor, image);
+ }
+ } else if (data.src) {
+ insertImageAtCaret(editor, data);
+ }
+ };
+
+ var updateVSpaceHSpaceBorder = function (editor) {
+ return function (evt) {
+ var dom = editor.dom;
+ var rootControl = evt.control.rootControl;
+ if (!$_bbugoqctjm7ol69d.hasAdvTab(editor)) {
+ return;
+ }
+ var data = rootControl.toJSON();
+ var css = dom.parseStyle(data.style);
+ rootControl.find('#vspace').value('');
+ rootControl.find('#hspace').value('');
+ css = $_b0dhuucujm7ol69f.mergeMargins(css);
+ if (css['margin-top'] && css['margin-bottom'] || css['margin-right'] && css['margin-left']) {
+ if (css['margin-top'] === css['margin-bottom']) {
+ rootControl.find('#vspace').value($_b0dhuucujm7ol69f.removePixelSuffix(css['margin-top']));
+ } else {
+ rootControl.find('#vspace').value('');
+ }
+ if (css['margin-right'] === css['margin-left']) {
+ rootControl.find('#hspace').value($_b0dhuucujm7ol69f.removePixelSuffix(css['margin-right']));
+ } else {
+ rootControl.find('#hspace').value('');
+ }
+ }
+ if (css['border-width']) {
+ rootControl.find('#border').value($_b0dhuucujm7ol69f.removePixelSuffix(css['border-width']));
+ } else {
+ rootControl.find('#border').value('');
+ }
+ if (css['border-style']) {
+ rootControl.find('#borderStyle').value(css['border-style']);
+ } else {
+ rootControl.find('#borderStyle').value('');
+ }
+ rootControl.find('#style').value(dom.serializeStyle(dom.parseStyle(dom.serializeStyle(css))));
+ };
+ };
+ var updateStyle = function (editor, win) {
+ win.find('#style').each(function (ctrl) {
+ var value = getStyleValue(function (css) {
+ return normalizeCss(editor, css);
+ }, merge(defaultData(), win.toJSON()));
+ ctrl.value(value);
+ });
+ };
+ var makeTab = function (editor) {
+ return {
+ title: 'Advanced',
+ type: 'form',
+ pack: 'start',
+ items: [
+ {
+ label: 'Style',
+ name: 'style',
+ type: 'textbox',
+ onchange: updateVSpaceHSpaceBorder(editor)
+ },
+ {
+ type: 'form',
+ layout: 'grid',
+ packV: 'start',
+ columns: 2,
+ padding: 0,
+ defaults: {
+ type: 'textbox',
+ maxWidth: 50,
+ onchange: function (evt) {
+ updateStyle(editor, evt.control.rootControl);
+ }
+ },
+ items: [
+ {
+ label: 'Vertical space',
+ name: 'vspace'
+ },
+ {
+ label: 'Border width',
+ name: 'border'
+ },
+ {
+ label: 'Horizontal space',
+ name: 'hspace'
+ },
+ {
+ label: 'Border style',
+ type: 'listbox',
+ name: 'borderStyle',
+ width: 90,
+ maxWidth: 90,
+ onselect: function (evt) {
+ updateStyle(editor, evt.control.rootControl);
+ },
+ values: [
+ {
+ text: 'Select...',
+ value: ''
+ },
+ {
+ text: 'Solid',
+ value: 'solid'
+ },
+ {
+ text: 'Dotted',
+ value: 'dotted'
+ },
+ {
+ text: 'Dashed',
+ value: 'dashed'
+ },
+ {
+ text: 'Double',
+ value: 'double'
+ },
+ {
+ text: 'Groove',
+ value: 'groove'
+ },
+ {
+ text: 'Ridge',
+ value: 'ridge'
+ },
+ {
+ text: 'Inset',
+ value: 'inset'
+ },
+ {
+ text: 'Outset',
+ value: 'outset'
+ },
+ {
+ text: 'None',
+ value: 'none'
+ },
+ {
+ text: 'Hidden',
+ value: 'hidden'
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ };
+ };
+ var $_gxiigd3jm7ol6a8 = { makeTab: makeTab };
+
+ var doSyncSize = function (widthCtrl, heightCtrl) {
+ widthCtrl.state.set('oldVal', widthCtrl.value());
+ heightCtrl.state.set('oldVal', heightCtrl.value());
+ };
+ var doSizeControls = function (win, f) {
+ var widthCtrl = win.find('#width')[0];
+ var heightCtrl = win.find('#height')[0];
+ var constrained = win.find('#constrain')[0];
+ if (widthCtrl && heightCtrl && constrained) {
+ f(widthCtrl, heightCtrl, constrained.checked());
+ }
+ };
+ var doUpdateSize = function (widthCtrl, heightCtrl, isContrained) {
+ var oldWidth = widthCtrl.state.get('oldVal');
+ var oldHeight = heightCtrl.state.get('oldVal');
+ var newWidth = widthCtrl.value();
+ var newHeight = heightCtrl.value();
+ if (isContrained && oldWidth && oldHeight && newWidth && newHeight) {
+ if (newWidth !== oldWidth) {
+ newHeight = Math.round(newWidth / oldWidth * newHeight);
+ if (!isNaN(newHeight)) {
+ heightCtrl.value(newHeight);
+ }
+ } else {
+ newWidth = Math.round(newHeight / oldHeight * newWidth);
+ if (!isNaN(newWidth)) {
+ widthCtrl.value(newWidth);
+ }
+ }
+ }
+ doSyncSize(widthCtrl, heightCtrl);
+ };
+ var syncSize = function (win) {
+ doSizeControls(win, doSyncSize);
+ };
+ var updateSize = function (win) {
+ doSizeControls(win, doUpdateSize);
+ };
+ var createUi = function () {
+ var recalcSize = function (evt) {
+ updateSize(evt.control.rootControl);
+ };
+ return {
+ type: 'container',
+ label: 'Dimensions',
+ layout: 'flex',
+ align: 'center',
+ spacing: 5,
+ items: [
+ {
+ name: 'width',
+ type: 'textbox',
+ maxLength: 5,
+ size: 5,
+ onchange: recalcSize,
+ ariaLabel: 'Width'
+ },
+ {
+ type: 'label',
+ text: 'x'
+ },
+ {
+ name: 'height',
+ type: 'textbox',
+ maxLength: 5,
+ size: 5,
+ onchange: recalcSize,
+ ariaLabel: 'Height'
+ },
+ {
+ name: 'constrain',
+ type: 'checkbox',
+ checked: true,
+ text: 'Constrain proportions'
+ }
+ ]
+ };
+ };
+ var $_q1xwtdajm7ol6ap = {
+ createUi: createUi,
+ syncSize: syncSize,
+ updateSize: updateSize
+ };
+
+ var onSrcChange = function (evt, editor) {
+ var srcURL, prependURL, absoluteURLPattern;
+ var meta = evt.meta || {};
+ var control = evt.control;
+ var rootControl = control.rootControl;
+ var imageListCtrl = rootControl.find('#image-list')[0];
+ if (imageListCtrl) {
+ imageListCtrl.value(editor.convertURL(control.value(), 'src'));
+ }
+ global$2.each(meta, function (value, key) {
+ rootControl.find('#' + key).value(value);
+ });
+ if (!meta.width && !meta.height) {
+ srcURL = editor.convertURL(control.value(), 'src');
+ prependURL = $_bbugoqctjm7ol69d.getPrependUrl(editor);
+ absoluteURLPattern = new RegExp('^(?:[a-z]+:)?//', 'i');
+ if (prependURL && !absoluteURLPattern.test(srcURL) && srcURL.substring(0, prependURL.length) !== prependURL) {
+ srcURL = prependURL + srcURL;
+ }
+ control.value(srcURL);
+ $_b0dhuucujm7ol69f.getImageSize(editor.documentBaseURI.toAbsolute(control.value()), function (data) {
+ if (data.width && data.height && $_bbugoqctjm7ol69d.hasDimensions(editor)) {
+ rootControl.find('#width').value(data.width);
+ rootControl.find('#height').value(data.height);
+ $_q1xwtdajm7ol6ap.syncSize(rootControl);
+ }
+ });
+ }
+ };
+ var onBeforeCall = function (evt) {
+ evt.meta = evt.control.rootControl.toJSON();
+ };
+ var getGeneralItems = function (editor, imageListCtrl) {
+ var generalFormItems = [
+ {
+ name: 'src',
+ type: 'filepicker',
+ filetype: 'image',
+ label: 'Source',
+ autofocus: true,
+ onchange: function (evt) {
+ onSrcChange(evt, editor);
+ },
+ onbeforecall: onBeforeCall
+ },
+ imageListCtrl
+ ];
+ if ($_bbugoqctjm7ol69d.hasDescription(editor)) {
+ generalFormItems.push({
+ name: 'alt',
+ type: 'textbox',
+ label: 'Image description'
+ });
+ }
+ if ($_bbugoqctjm7ol69d.hasImageTitle(editor)) {
+ generalFormItems.push({
+ name: 'title',
+ type: 'textbox',
+ label: 'Image Title'
+ });
+ }
+ if ($_bbugoqctjm7ol69d.hasDimensions(editor)) {
+ generalFormItems.push($_q1xwtdajm7ol6ap.createUi());
+ }
+ if ($_bbugoqctjm7ol69d.getClassList(editor)) {
+ generalFormItems.push({
+ name: 'class',
+ type: 'listbox',
+ label: 'Class',
+ values: $_b0dhuucujm7ol69f.buildListItems($_bbugoqctjm7ol69d.getClassList(editor), function (item) {
+ if (item.value) {
+ item.textStyle = function () {
+ return editor.formatter.getCssText({
+ inline: 'img',
+ classes: [item.value]
+ });
+ };
+ }
+ })
+ });
+ }
+ if ($_bbugoqctjm7ol69d.hasImageCaption(editor)) {
+ generalFormItems.push({
+ name: 'caption',
+ type: 'checkbox',
+ label: 'Caption'
+ });
+ }
+ return generalFormItems;
+ };
+ var makeTab$1 = function (editor, imageListCtrl) {
+ return {
+ title: 'General',
+ type: 'form',
+ items: getGeneralItems(editor, imageListCtrl)
+ };
+ };
+ var $_3lygvvd9jm7ol6an = {
+ makeTab: makeTab$1,
+ getGeneralItems: getGeneralItems
+ };
+
+ var url = function () {
+ return $_g9oowjcwjm7ol69l.getOrDie('URL');
+ };
+ var createObjectURL = function (blob) {
+ return url().createObjectURL(blob);
+ };
+ var revokeObjectURL = function (u) {
+ url().revokeObjectURL(u);
+ };
+ var $_15emksdcjm7ol6au = {
+ createObjectURL: createObjectURL,
+ revokeObjectURL: revokeObjectURL
+ };
+
+ var global$5 = tinymce.util.Tools.resolve('tinymce.ui.Factory');
+
+ function XMLHttpRequest () {
+ var f = $_g9oowjcwjm7ol69l.getOrDie('XMLHttpRequest');
+ return new f();
+ }
+
+ var noop = function () {
+ };
+ var pathJoin = function (path1, path2) {
+ if (path1) {
+ return path1.replace(/\/$/, '') + '/' + path2.replace(/^\//, '');
+ }
+ return path2;
+ };
+ function Uploader (settings) {
+ var defaultHandler = function (blobInfo, success, failure, progress) {
+ var xhr, formData;
+ xhr = new XMLHttpRequest();
+ xhr.open('POST', settings.url);
+ xhr.withCredentials = settings.credentials;
+ xhr.upload.onprogress = function (e) {
+ progress(e.loaded / e.total * 100);
+ };
+ xhr.onerror = function () {
+ failure('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
+ };
+ xhr.onload = function () {
+ var json;
+ if (xhr.status < 200 || xhr.status >= 300) {
+ failure('HTTP Error: ' + xhr.status);
+ return;
+ }
+ json = JSON.parse(xhr.responseText);
+ if (!json || typeof json.location !== 'string') {
+ failure('Invalid JSON: ' + xhr.responseText);
+ return;
+ }
+ success(pathJoin(settings.basePath, json.location));
+ };
+ formData = new FormData();
+ formData.append('file', blobInfo.blob(), blobInfo.filename());
+ xhr.send(formData);
+ };
+ var uploadBlob = function (blobInfo, handler) {
+ return new global$1(function (resolve, reject) {
+ try {
+ handler(blobInfo, resolve, reject, noop);
+ } catch (ex) {
+ reject(ex.message);
+ }
+ });
+ };
+ var isDefaultHandler = function (handler) {
+ return handler === defaultHandler;
+ };
+ var upload = function (blobInfo) {
+ return !settings.url && isDefaultHandler(settings.handler) ? global$1.reject('Upload url missing from the settings.') : uploadBlob(blobInfo, settings.handler);
+ };
+ settings = global$2.extend({
+ credentials: false,
+ handler: defaultHandler
+ }, settings);
+ return { upload: upload };
+ }
+
+ var onFileInput = function (editor) {
+ return function (evt) {
+ var Throbber = global$5.get('Throbber');
+ var rootControl = evt.control.rootControl;
+ var throbber = new Throbber(rootControl.getEl());
+ var file = evt.control.value();
+ var blobUri = $_15emksdcjm7ol6au.createObjectURL(file);
+ var uploader = Uploader({
+ url: $_bbugoqctjm7ol69d.getUploadUrl(editor),
+ basePath: $_bbugoqctjm7ol69d.getUploadBasePath(editor),
+ credentials: $_bbugoqctjm7ol69d.getUploadCredentials(editor),
+ handler: $_bbugoqctjm7ol69d.getUploadHandler(editor)
+ });
+ var finalize = function () {
+ throbber.hide();
+ $_15emksdcjm7ol6au.revokeObjectURL(blobUri);
+ };
+ throbber.show();
+ return $_b0dhuucujm7ol69f.blobToDataUri(file).then(function (dataUrl) {
+ var blobInfo = editor.editorUpload.blobCache.create({
+ blob: file,
+ blobUri: blobUri,
+ name: file.name ? file.name.replace(/\.[^\.]+$/, '') : null,
+ base64: dataUrl.split(',')[1]
+ });
+ return uploader.upload(blobInfo).then(function (url) {
+ var src = rootControl.find('#src');
+ src.value(url);
+ rootControl.find('tabpanel')[0].activateTab(0);
+ src.fire('change');
+ finalize();
+ return url;
+ });
+ }).catch(function (err) {
+ editor.windowManager.alert(err);
+ finalize();
+ });
+ };
+ };
+ var acceptExts = '.jpg,.jpeg,.png,.gif';
+ var makeTab$2 = function (editor) {
+ return {
+ title: 'Upload',
+ type: 'form',
+ layout: 'flex',
+ direction: 'column',
+ align: 'stretch',
+ padding: '20 20 20 20',
+ items: [
+ {
+ type: 'container',
+ layout: 'flex',
+ direction: 'column',
+ align: 'center',
+ spacing: 10,
+ items: [
+ {
+ text: 'Browse for an image',
+ type: 'browsebutton',
+ accept: acceptExts,
+ onchange: onFileInput(editor)
+ },
+ {
+ text: 'OR',
+ type: 'label'
+ }
+ ]
+ },
+ {
+ text: 'Drop an image here',
+ type: 'dropzone',
+ accept: acceptExts,
+ height: 100,
+ onchange: onFileInput(editor)
+ }
+ ]
+ };
+ };
+ var $_2dxlkydbjm7ol6ar = { makeTab: makeTab$2 };
+
+ var curry = function (f) {
+ var x = [];
+ for (var _i = 1; _i < arguments.length; _i++) {
+ x[_i - 1] = arguments[_i];
+ }
+ var args = new Array(arguments.length - 1);
+ for (var i = 1; i < arguments.length; i++)
+ args[i - 1] = arguments[i];
+ return function () {
+ var x = [];
+ for (var _i = 0; _i < arguments.length; _i++) {
+ x[_i] = arguments[_i];
+ }
+ var newArgs = new Array(arguments.length);
+ for (var j = 0; j < newArgs.length; j++)
+ newArgs[j] = arguments[j];
+ var all = args.concat(newArgs);
+ return f.apply(null, all);
+ };
+ };
+
+ var submitForm = function (editor, evt) {
+ var win = evt.control.getRoot();
+ $_q1xwtdajm7ol6ap.updateSize(win);
+ editor.undoManager.transact(function () {
+ var data = merge(readImageDataFromSelection(editor), win.toJSON());
+ insertOrUpdateImage(editor, data);
+ });
+ editor.editorUpload.uploadImagesAuto();
+ };
+ function Dialog (editor) {
+ function showDialog(imageList) {
+ var data = readImageDataFromSelection(editor);
+ var win, imageListCtrl;
+ if (imageList) {
+ imageListCtrl = {
+ type: 'listbox',
+ label: 'Image list',
+ name: 'image-list',
+ values: $_b0dhuucujm7ol69f.buildListItems(imageList, function (item) {
+ item.value = editor.convertURL(item.value || item.url, 'src');
+ }, [{
+ text: 'None',
+ value: ''
+ }]),
+ value: data.src && editor.convertURL(data.src, 'src'),
+ onselect: function (e) {
+ var altCtrl = win.find('#alt');
+ if (!altCtrl.value() || e.lastControl && altCtrl.value() === e.lastControl.text()) {
+ altCtrl.value(e.control.text());
+ }
+ win.find('#src').value(e.control.value()).fire('change');
+ },
+ onPostRender: function () {
+ imageListCtrl = this;
+ }
+ };
+ }
+ if ($_bbugoqctjm7ol69d.hasAdvTab(editor) || $_bbugoqctjm7ol69d.hasUploadUrl(editor) || $_bbugoqctjm7ol69d.hasUploadHandler(editor)) {
+ var body = [$_3lygvvd9jm7ol6an.makeTab(editor, imageListCtrl)];
+ if ($_bbugoqctjm7ol69d.hasAdvTab(editor)) {
+ body.push($_gxiigd3jm7ol6a8.makeTab(editor));
+ }
+ if ($_bbugoqctjm7ol69d.hasUploadUrl(editor) || $_bbugoqctjm7ol69d.hasUploadHandler(editor)) {
+ body.push($_2dxlkydbjm7ol6ar.makeTab(editor));
+ }
+ win = editor.windowManager.open({
+ title: 'Insert/edit image',
+ data: data,
+ bodyType: 'tabpanel',
+ body: body,
+ onSubmit: curry(submitForm, editor)
+ });
+ } else {
+ win = editor.windowManager.open({
+ title: 'Insert/edit image',
+ data: data,
+ body: $_3lygvvd9jm7ol6an.getGeneralItems(editor, imageListCtrl),
+ onSubmit: curry(submitForm, editor)
+ });
+ }
+ $_q1xwtdajm7ol6ap.syncSize(win);
+ }
+ function open() {
+ $_b0dhuucujm7ol69f.createImageList(editor, showDialog);
+ }
+ return { open: open };
+ }
+
+ var register = function (editor) {
+ editor.addCommand('mceImage', Dialog(editor).open);
+ };
+ var $_1y4ugocrjm7ol697 = { register: register };
+
+ var hasImageClass = function (node) {
+ var className = node.attr('class');
+ return className && /\bimage\b/.test(className);
+ };
+ var toggleContentEditableState = function (state) {
+ return function (nodes) {
+ var i = nodes.length, node;
+ var toggleContentEditable = function (node) {
+ node.attr('contenteditable', state ? 'true' : null);
+ };
+ while (i--) {
+ node = nodes[i];
+ if (hasImageClass(node)) {
+ node.attr('contenteditable', state ? 'false' : null);
+ global$2.each(node.getAll('figcaption'), toggleContentEditable);
+ }
+ }
+ };
+ };
+ var setup = function (editor) {
+ editor.on('preInit', function () {
+ editor.parser.addNodeFilter('figure', toggleContentEditableState(true));
+ editor.serializer.addNodeFilter('figure', toggleContentEditableState(false));
+ });
+ };
+ var $_bvrzx8dhjm7ol6b0 = { setup: setup };
+
+ var register$1 = function (editor) {
+ editor.addButton('image', {
+ icon: 'image',
+ tooltip: 'Insert/edit image',
+ onclick: Dialog(editor).open,
+ stateSelector: 'img:not([data-mce-object],[data-mce-placeholder]),figure.image'
+ });
+ editor.addMenuItem('image', {
+ icon: 'image',
+ text: 'Image',
+ onclick: Dialog(editor).open,
+ context: 'insert',
+ prependToContext: true
+ });
+ };
+ var $_41pjz5dijm7ol6b1 = { register: register$1 };
+
+ global.add('image', function (editor) {
+ $_bvrzx8dhjm7ol6b0.setup(editor);
+ $_41pjz5dijm7ol6b1.register(editor);
+ $_1y4ugocrjm7ol697.register(editor);
+ });
+ function Plugin () {
+ }
+
+ return Plugin;
+
+}());
+})();
diff --git a/ccm-core/web/assets/tinymce/js/tinymce/plugins/trunk-images/plugin.min.js b/ccm-core/web/assets/tinymce/js/tinymce/plugins/trunk-images/plugin.min.js
deleted file mode 100644
index 0c57978da..000000000
--- a/ccm-core/web/assets/tinymce/js/tinymce/plugins/trunk-images/plugin.min.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(e){function t(i){if(n[i])return n[i].exports;var a=n[i]={i:i,l:!1,exports:{}};return e[i].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=2)}([function(e,t,n){"use strict";t.__esModule=!0;var i=n(1),a=function(e,t){e.addButton("trunk-images-button",{icon:"image",onlick:i.default(e).open,stateSelector:"div.image"}),e.addMenuItem("trunk-images",{icon:"image",text:"Insert Images",onclick:i.default(e).open,stateSelector:"image",context:"insert",prependToContext:!0})};t.default=a},function(e,t,n){"use strict";function i(e){function t(){var t="",n=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),i=new tinymce.ui.TextBox({name:"file",label:"File:",disabled:!0});n.add(i);var a=new tinymce.ui.Button({name:"browse_images",text:"Browse Images",onclick:function(){var e=window.location.href,n=e.lastIndexOf("/"),a=e.slice(0,n+1)+"image_select.jsp";window.open(a,"_blank","scrollbars=yes,directories=no,toolbar=no,width=800,height=600,status=no,menubar=no");window.openCCM=new Object,window.openCCM.imageSet=function(e){return i.text(e.src),p.find("#file").value(e.src).fire("change"),p.find("#width").value(e.width).fire("change"),p.find("#height").value(e.height).fire("change"),t=e.name,!0}}});n.add(a);var o=new tinymce.ui.TextBox({name:"alternate",label:"Alternate:"}),l=new tinymce.ui.TextBox({name:"title",label:"Title:"}),r=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),u=new tinymce.ui.Label({text:"Alignment:"}),c=new tinymce.ui.ListBox({name:"alignment",values:[{text:"Not set",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]});r.add(u),r.add(c);var d=new tinymce.ui.Container({type:"container",layout:"flex",direction:"row"}),s=new tinymce.ui.Label({text:"Fancy Box:"}),m=new tinymce.ui.ListBox({name:"fancybox",values:[{text:"None",value:""},{text:"Zoom",value:"imageZoom"},{text:"Gallery",value:"imageGallery"}]});d.add(s),d.add(m);var f=new tinymce.ui.Checkbox({label:"Caption:",name:"caption"}),g=new tinymce.ui.Container({label:"Dimension",layout:"flex",direction:"row",align:"center",padding:5,spacing:15,margin:5}),x=new tinymce.ui.TextBox({name:"width",label:"Width"}),w=new tinymce.ui.TextBox({name:"height",label:"Height"});g.add(x),g.add({type:"label",text:"X"}),g.add(w);var p=e.windowManager.open({title:"Insert/Modify Image",width:800,height:600,body:[n,o,l,r,d,f,g],onsubmit:function(){var n=p.find("#file").value(),i=p.find("#alternate").value(),a=p.find("#width").value(),o=p.find("#height").value(),l=p.find("#title").value(),r=p.find("#alignment").value(),u=p.find("#fancybox").value();if(null!=n){var c="',d=" '+c+"",s="";p.find("#caption").value()&&(s=''+t+"");var m='
")}}})}return{open:t}}t.__esModule=!0,t.default=i},function(e,t,n){"use strict";t.__esModule=!0;var i=n(0);tinymce.PluginManager.add("trunk-images",i.default)}]);
\ No newline at end of file
diff --git a/ccm-core/web/assets/tinymce/js/tinymce/plugins/trunk-images/plugin.min.min.js b/ccm-core/web/assets/tinymce/js/tinymce/plugins/trunk-images/plugin.min.min.js
deleted file mode 100644
index 996f334a6..000000000
--- a/ccm-core/web/assets/tinymce/js/tinymce/plugins/trunk-images/plugin.min.min.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(t){function e(o){if(n[o])return n[o].exports;var u=n[o]={i:o,l:!1,exports:{}};return t[o].call(u.exports,u,u.exports,e),u.l=!0,u.exports}var n={};e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,n){"use strict";e.__esModule=!0;var o=n(1),u=function(t,e){return console.log("Trunk-Images loaded"),t.addMenuItem("trunk-images",{icon:!1,text:"Insert Trunk-Images",onclick:function(){o.default.open(t)},context:"insert"}),{}};e.default=u},function(t,e,n){"use strict";e.__esModule=!0;var o=function(t){t.addMenuItem("example",{text:"Example plugin",context:"tools",onclick:function(){t.windowManager.open({title:"TinyMCE site",url:"https://www.tinymce.com",width:800,height:600,buttons:[{text:"Close",onclick:"close"}]})}})};e.default={open:o}},function(t,e,n){"use strict";e.__esModule=!0;var o=n(0);tinymce.PluginManager.add("trunk-images",o.default)}]);
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/.editorconfig b/tools-ng/tinymce/editor/.editorconfig
new file mode 100644
index 000000000..04281c366
--- /dev/null
+++ b/tools-ng/tinymce/editor/.editorconfig
@@ -0,0 +1,8 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+end_of_line = lf
diff --git a/tools-ng/tinymce/editor/.gitattributes b/tools-ng/tinymce/editor/.gitattributes
new file mode 100644
index 000000000..e2f482943
--- /dev/null
+++ b/tools-ng/tinymce/editor/.gitattributes
@@ -0,0 +1,16 @@
+* eol=lf
+*.jar binary
+*.gif binary
+*.png binary
+*.jpg binary
+*.swf binary
+*.xap binary
+*.zip binary
+*.eot binary
+*.woff binary
+*.ttf binary
+*.mov binary
+*.avi binary
+*.flv binary
+*.rm binary
+*.dcr binary
diff --git a/tools-ng/tinymce/editor/.github/ISSUE_TEMPLATE.md b/tools-ng/tinymce/editor/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..ddbadeb25
--- /dev/null
+++ b/tools-ng/tinymce/editor/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,9 @@
+**Do you want to request a *feature* or report a *bug*?**
+
+**What is the current behavior?**
+
+**If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem via [fiddle.tinymce.com](http://fiddle.tinymce.com/) or similar.**
+
+**What is the expected behavior?**
+
+**Which versions of TinyMCE, and which browser / OS are affected by this issue? Did this work in previous versions of TinyMCE?**
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/.github/PULL_REQUEST_TEMPLATE.md b/tools-ng/tinymce/editor/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 000000000..350621ffe
--- /dev/null
+++ b/tools-ng/tinymce/editor/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,7 @@
+**Before submitting a pull request** please do the following:
+
+1. Fork [the repository](https://github.com/tinymce/tinymce) and create your branch from `master`
+2. Have you added some code that should be tested? Write some tests! (Are you unsure how to write the test you want to write, ask us for help!)
+3. Ensure that the tests pass: `grunt test`
+4. Ensure that your code passes the linter: `grunt lint`
+5. Make sure to sign the CLA.
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/.gitignore b/tools-ng/tinymce/editor/.gitignore
new file mode 100644
index 000000000..37819b512
--- /dev/null
+++ b/tools-ng/tinymce/editor/.gitignore
@@ -0,0 +1,43 @@
+bin
+obj
+tmp
+.settings
+.idea
+.project
+.vscode
+.nyc_output
+*.sublime-*
+.externalToolBuilders
+**/bolt/bootstrap*.js
+*~
+*.diff
+*.patch
+*.bak
+*.log
+*.swp
+yarn.lock
+package-lock.json
+.DS_Store
+.project
+coverage
+node_modules
+js/**/*
+imagemanager
+filemanager
+mcmanager
+powerpaste
+tinymcespellchecker
+a11ychecker
+codemirror
+mentions
+*.min.js
+*.dev.js
+*.full.js
+*.min.css
+*.dev.less
+**/dist
+**/scratch
+**/lib
+**/dependency
+**/instrumented_*
+.rpt2_cache
diff --git a/tools-ng/tinymce/editor/Gruntfile.js b/tools-ng/tinymce/editor/Gruntfile.js
new file mode 100644
index 000000000..6d814e5a6
--- /dev/null
+++ b/tools-ng/tinymce/editor/Gruntfile.js
@@ -0,0 +1,825 @@
+/*eslint-env node */
+
+let zipUtils = require('./tools/modules/zip-helper');
+let gruntUtils = require('./tools/modules/grunt-utils');
+let gruntWebPack = require('./tools/modules/grunt-webpack');
+let swag = require('@ephox/swag');
+let path = require('path');
+
+let plugins = [
+ 'advlist', 'anchor', 'autolink', 'autoresize', 'autosave', 'bbcode', 'charmap', 'code', 'codesample',
+ 'colorpicker', /*'compat3x', */ 'contextmenu', 'directionality', 'emoticons', 'help', 'fullpage',
+ 'fullscreen', 'hr', 'image', 'imagetools', 'importcss', 'insertdatetime', 'legacyoutput', 'link',
+ 'lists', 'media', 'nonbreaking', 'noneditable', 'pagebreak', 'paste', 'preview', 'print', 'save',
+ 'searchreplace', 'spellchecker', 'tabfocus', 'table', 'template', 'textcolor', 'textpattern', 'toc',
+ 'visualblocks', 'visualchars', 'wordcount',
+];
+
+let themes = [
+ 'modern', 'mobile', 'inlite'
+];
+
+module.exports = function (grunt) {
+ var packageData = grunt.file.readJSON('package.json');
+ var changelogLine = grunt.file.read('changelog.txt').toString().split('\n')[0];
+ var BUILD_VERSION = packageData.version + '-' + (process.env.BUILD_NUMBER ? process.env.BUILD_NUMBER : '0');
+ packageData.date = /^Version [^\(]+\(([^\)]+)\)/.exec(changelogLine)[1];
+
+ grunt.initConfig({
+ pkg: packageData,
+
+ shell: {
+ tsc: { command: 'node ./node_modules/typescript/bin/tsc' }
+ },
+
+ tslint: {
+ options: {
+ configuration: 'tslint.json'
+ },
+ files: { src: [ 'src/**/*.ts' ] }
+ },
+
+ globals: {
+ options: {
+ configFile: 'src/core/main/json/globals.json',
+ outputDir: 'lib/globals',
+ templateFile: 'src/core/main/js/GlobalsTemplate.js'
+ }
+ },
+
+ rollup: Object.assign(
+ {
+ core: {
+ options: {
+ treeshake: true,
+ name: 'tinymce',
+ format: 'iife',
+ banner: '(function () {',
+ footer: '})();',
+ plugins: [
+ swag.nodeResolve({
+ basedir: __dirname,
+ prefixes: {
+ 'tinymce/core': 'lib/core/main/ts'
+ }
+ }),
+ swag.remapImports()
+ ]
+ },
+ files:[
+ {
+ src: 'lib/core/main/ts/api/Main.js',
+ dest: 'js/tinymce/tinymce.js'
+ }
+ ]
+ }
+ },
+ gruntUtils.generate(plugins, 'plugin', (name) => {
+ return {
+ options: {
+ treeshake: true,
+ name: name,
+ format: 'iife',
+ banner: '(function () {',
+ footer: '})();',
+ plugins: [
+ swag.nodeResolve({
+ basedir: __dirname,
+ prefixes: gruntUtils.prefixes({
+ 'tinymce/core': 'lib/globals/tinymce/core'
+ }, [
+ [`tinymce/plugins/${name}`, `lib/plugins/${name}/main/ts`]
+ ])
+ }),
+ swag.remapImports()
+ ]
+ },
+ files:[ { src: `lib/plugins/${name}/main/ts/Plugin.js`, dest: `js/tinymce/plugins/${name}/plugin.js` } ]
+ };
+ }),
+ gruntUtils.generate(themes, 'theme', (name) => {
+ return {
+ options: {
+ treeshake: true,
+ name: name,
+ format: 'iife',
+ banner: '(function () {',
+ footer: '})();',
+ plugins: [
+ swag.nodeResolve({
+ basedir: __dirname,
+ prefixes: gruntUtils.prefixes({
+ 'tinymce/core': 'lib/globals/tinymce/core',
+ 'tinymce/ui': 'lib/ui/main/ts'
+ }, [
+ [`tinymce/themes/${name}`, `lib/themes/${name}/main/ts`]
+ ])
+ }),
+ swag.remapImports()
+ ]
+ },
+ files:[
+ {
+ src: `lib/themes/${name}/main/ts/Theme.js`,
+ dest: `js/tinymce/themes/${name}/theme.js`
+ }
+ ]
+ };
+ })
+ ),
+
+ uglify: Object.assign(
+ {
+ options: {
+ output: {
+ ascii_only: true,
+ },
+ ie8: true
+ },
+ core: {
+ files: [
+ { src: 'js/tinymce/tinymce.js', dest: 'js/tinymce/tinymce.min.js' },
+ { src: 'src/core/main/js/JqueryIntegration.js', dest: 'js/tinymce/jquery.tinymce.min.js' }
+ ]
+ },
+ 'compat3x-plugin': {
+ files: [
+ {
+ src: 'src/plugins/compat3x/main/js/plugin.js',
+ dest: 'js/tinymce/plugins/compat3x/plugin.min.js'
+ }
+ ]
+ }
+ },
+ gruntUtils.generate(plugins, 'plugin', (name) => {
+ return {
+ files: [ { src: `js/tinymce/plugins/${name}/plugin.js`, dest: `js/tinymce/plugins/${name}/plugin.min.js` } ]
+ };
+ }),
+ gruntUtils.generate(themes, 'theme', (name) => {
+ return {
+ files: [ { src: `js/tinymce/themes/${name}/theme.js`, dest: `js/tinymce/themes/${name}/theme.min.js` } ]
+ };
+ })
+ ),
+
+ webpack: Object.assign(
+ {core: () => gruntWebPack.create('src/core/demo/ts/demo/Demos.ts', 'tsconfig.json', 'scratch/demos/core', 'demo.js')},
+ {plugins: () => gruntWebPack.allPlugins(plugins)},
+ {themes: () => gruntWebPack.allThemes(themes)},
+ gruntUtils.generate(plugins, 'plugin', (name) => () => gruntWebPack.createPlugin(name) ),
+ gruntUtils.generate(themes, 'theme', (name) => () => gruntWebPack.createTheme(name) )
+ ),
+
+ 'webpack-dev-server': {
+ options: {
+ webpack: gruntWebPack.all(plugins, themes),
+ publicPath: '/',
+ inline: false,
+ port: grunt.option('webpack-port') !== undefined ? grunt.option('webpack-port') : 3000,
+ host: '0.0.0.0',
+ disableHostCheck: true,
+ before: app => gruntWebPack.generateDemoIndex(grunt, app, plugins, themes)
+ },
+ start: { }
+ },
+
+ less: {
+ desktop: {
+ options: {
+ cleancss: true,
+ strictImports: true,
+ compress: true,
+ yuicompress: true,
+ sourceMap: true,
+ sourceMapRootpath: '.',
+ optimization: 2
+ },
+ files: {
+ 'js/tinymce/skins/lightgray/skin.min.css': 'src/skins/lightgray/main/less/desktop/Skin.less'
+ }
+ },
+ mobile: {
+ options: {
+ plugins : [ new (require('less-plugin-autoprefix'))({ browsers : [ 'last 2 versions', /* for phantom */'safari >= 4' ] }) ],
+ compress: true,
+ yuicompress: true,
+ sourceMap: true,
+ sourceMapRootpath: '.',
+ optimization: 2
+ },
+ files: {
+ 'js/tinymce/skins/lightgray/skin.mobile.min.css': 'src/skins/lightgray/main/less/mobile/app/mobile-less.less'
+ }
+ },
+ 'content-mobile': {
+ options: {
+ cleancss: true,
+ strictImports: true,
+ compress: true
+ },
+ files: {
+ 'js/tinymce/skins/lightgray/content.mobile.min.css': 'src/skins/lightgray/main/less/mobile/content.less'
+ }
+ },
+ content: {
+ options: {
+ cleancss: true,
+ strictImports: true,
+ compress: true
+ },
+ files: {
+ 'js/tinymce/skins/lightgray/content.min.css': 'src/skins/lightgray/main/less/desktop/Content.less'
+ }
+ },
+ 'content-inline': {
+ options: {
+ cleancss: true,
+ strictImports: true,
+ compress: true
+ },
+ files: {
+ 'js/tinymce/skins/lightgray/content.inline.min.css': 'src/skins/lightgray/main/less/desktop/Content.Inline.less'
+ }
+ }
+ },
+
+ copy: {
+ core: {
+ options: {
+ process: function (content) {
+ return content.
+ replace('@@majorVersion@@', packageData.version.split('.')[0]).
+ replace('@@minorVersion@@', packageData.version.split('.').slice(1).join('.')).
+ replace('@@releaseDate@@', packageData.date);
+ }
+ },
+ files: [
+ {
+ src: 'js/tinymce/tinymce.js',
+ dest: 'js/tinymce/tinymce.js'
+ },
+ {
+ src: 'js/tinymce/tinymce.min.js',
+ dest: 'js/tinymce/tinymce.min.js'
+ },
+ {
+ src: 'src/core/main/text/readme_lang.md',
+ dest: 'js/tinymce/langs/readme.md'
+ },
+ {
+ src: 'LICENSE.TXT',
+ dest: 'js/tinymce/license.txt'
+ }
+ ]
+ },
+ skins: {
+ files: [
+ {
+ expand: true,
+ flatten: true,
+ cwd: 'src/skins/lightgray/main/fonts',
+ src: [
+ '**',
+ '!*.json',
+ '!*.md'
+ ],
+ dest: 'js/tinymce/skins/lightgray/fonts'
+ },
+ {
+ expand: true,
+ flatten: true,
+ cwd: 'src/skins/lightgray/main/img',
+ src: '**',
+ dest: 'js/tinymce/skins/lightgray/img'
+ }
+ ]
+ },
+ plugins: {
+ files: [
+ { expand: true, cwd: 'src/plugins/compat3x/main', src: ['img/**'], dest: 'js/tinymce/plugins/compat3x' },
+ { expand: true, cwd: 'src/plugins/compat3x/main', src: ['css/**'], dest: 'js/tinymce/plugins/compat3x' },
+ { expand: true, cwd: 'src/plugins/compat3x/main/js', src: ['utils/**', 'plugin.js', 'tiny_mce_popup.js'], dest: 'js/tinymce/plugins/compat3x' },
+ { src: 'src/plugins/codesample/main/css/prism.css', dest: 'js/tinymce/plugins/codesample/css/prism.css' }
+ ]
+ },
+ 'emoticons-plugin': {
+ files: [
+ {
+ flatten: true,
+ expand: true,
+ cwd: 'src/plugins/emoticons/main/img',
+ src: '*.gif',
+ dest: 'js/tinymce/plugins/emoticons/img/'
+ }
+ ]
+ },
+ 'help-plugin': {
+ files: [
+ { src: 'src/plugins/help/main/img/logo.png', dest: 'js/tinymce/plugins/help/img/logo.png' }
+ ]
+ },
+ 'visualblocks-plugin': {
+ files: [
+ { src: 'src/plugins/visualblocks/main/css/visualblocks.css', dest: 'js/tinymce/plugins/visualblocks/css/visualblocks.css' }
+ ]
+ }
+ },
+
+ moxiezip: {
+ production: {
+ options: {
+ baseDir: 'tinymce',
+ excludes: [
+ 'js/**/plugin.js',
+ 'js/**/theme.js',
+ 'js/**/*.map',
+ 'js/tinymce/tinymce.full.min.js',
+ 'js/tinymce/plugins/moxiemanager',
+ 'js/tinymce/plugins/compat3x',
+ 'js/tinymce/plugins/visualblocks/img',
+ 'js/tinymce/skins/*/fonts/*.json',
+ 'js/tinymce/skins/*/fonts/readme.md',
+ 'readme.md'
+ ],
+ to: 'tmp/tinymce_<%= pkg.version %>.zip'
+ },
+ src: [
+ 'js/tinymce/langs',
+ 'js/tinymce/plugins',
+ 'js/tinymce/skins',
+ 'js/tinymce/themes',
+ 'js/tinymce/tinymce.min.js',
+ 'js/tinymce/jquery.tinymce.min.js',
+ 'js/tinymce/license.txt',
+ 'changelog.txt',
+ 'LICENSE.TXT',
+ 'readme.md'
+ ]
+ },
+
+ development: {
+ options: {
+ baseDir: 'tinymce',
+ excludes: [
+ 'src/**/dist',
+ 'src/**/scratch',
+ 'src/**/lib',
+ 'src/**/dependency',
+ 'js/tinymce/tinymce.full.min.js',
+ 'js/tests/.jshintrc'
+ ],
+ to: 'tmp/tinymce_<%= pkg.version %>_dev.zip'
+ },
+ src: [
+ 'config',
+ 'src',
+ 'js',
+ 'tests',
+ 'tools',
+ 'changelog.txt',
+ 'LICENSE.TXT',
+ 'Gruntfile.js',
+ 'readme.md',
+ 'package.json',
+ '.eslintrc',
+ '.jscsrc',
+ '.jshintrc'
+ ]
+ },
+ cdn: {
+ options: {
+ onBeforeSave: function (zip) {
+ zip.addData('dist/version.txt', packageData.version);
+ },
+ pathFilter: function (zipFilePath) {
+ return zipFilePath.replace('js/tinymce/', 'dist/');
+ },
+ excludes: [
+ 'js/**/config',
+ 'js/**/scratch',
+ 'js/**/classes',
+ 'js/**/lib',
+ 'js/**/dependency',
+ 'js/**/src',
+ 'js/**/*.less',
+ 'js/**/*.dev.js',
+ 'js/**/*.dev.svg',
+ 'js/**/*.map',
+ 'js/tinymce/tinymce.full.min.js',
+ 'js/tinymce/plugins/moxiemanager',
+ 'js/tinymce/plugins/visualblocks/img',
+ 'js/tinymce/skins/*/fonts/*.json',
+ 'js/tinymce/skins/*/fonts/*.dev.svg',
+ 'js/tinymce/skins/*/fonts/readme.md',
+ 'readme.md',
+ 'js/tests/.jshintrc'
+ ],
+ concat: [
+ {
+ src: [
+ 'js/tinymce/tinymce.min.js',
+ 'js/tinymce/themes/*/theme.min.js',
+ 'js/tinymce/plugins/*/plugin.min.js',
+ '!js/tinymce/plugins/compat3x/plugin.min.js',
+ '!js/tinymce/plugins/example/plugin.min.js',
+ '!js/tinymce/plugins/example_dependency/plugin.min.js'
+ ],
+
+ dest: [
+ 'js/tinymce/tinymce.min.js'
+ ]
+ }
+ ],
+ to: 'tmp/tinymce_<%= pkg.version %>_cdn.zip'
+ },
+ src: [
+ 'js/tinymce/jquery.tinymce.min.js',
+ 'js/tinymce/tinymce.js',
+ 'js/tinymce/langs',
+ 'js/tinymce/plugins',
+ 'js/tinymce/skins',
+ 'js/tinymce/themes',
+ 'js/tinymce/license.txt'
+ ]
+ },
+
+ component: {
+ options: {
+ excludes: [
+ 'js/**/config',
+ 'js/**/scratch',
+ 'js/**/classes',
+ 'js/**/lib',
+ 'js/**/dependency',
+ 'js/**/src',
+ 'js/**/*.less',
+ 'js/**/*.dev.svg',
+ 'js/**/*.dev.js',
+ 'js/**/*.map',
+ 'js/tinymce/tinymce.full.min.js',
+ 'js/tinymce/plugins/moxiemanager',
+ 'js/tinymce/plugins/example',
+ 'js/tinymce/plugins/example_dependency',
+ 'js/tinymce/plugins/compat3x',
+ 'js/tinymce/plugins/visualblocks/img',
+ 'js/tinymce/skins/*/fonts/*.json',
+ 'js/tinymce/skins/*/fonts/readme.md'
+ ],
+ pathFilter: function (zipFilePath) {
+ if (zipFilePath.indexOf('js/tinymce/') === 0) {
+ return zipFilePath.substr('js/tinymce/'.length);
+ }
+
+ return zipFilePath;
+ },
+ onBeforeSave: function (zip) {
+ function jsonToBuffer(json) {
+ return new Buffer(JSON.stringify(json, null, '\t'));
+ }
+
+ zip.addData('bower.json', jsonToBuffer({
+ 'name': 'tinymce',
+ 'description': 'Web based JavaScript HTML WYSIWYG editor control.',
+ 'license': 'LGPL-2.1',
+ 'keywords': ['editor', 'wysiwyg', 'tinymce', 'richtext', 'javascript', 'html'],
+ 'homepage': 'http://www.tinymce.com',
+ 'ignore': ['readme.md', 'composer.json', 'package.json', '.npmignore', 'changelog.txt']
+ }));
+
+ zip.addData('package.json', jsonToBuffer({
+ 'name': 'tinymce',
+ 'version': packageData.version,
+ 'repository': {
+ 'type': 'git',
+ 'url': 'https://github.com/tinymce/tinymce-dist.git'
+ },
+ 'description': 'Web based JavaScript HTML WYSIWYG editor control.',
+ 'author': 'Ephox Corporation',
+ 'main': 'tinymce.js',
+ 'license': 'LGPL-2.1',
+ 'keywords': ['editor', 'wysiwyg', 'tinymce', 'richtext', 'javascript', 'html'],
+ 'bugs': { 'url': 'https://github.com/tinymce/tinymce/issues' }
+ }));
+
+ zip.addData('composer.json', jsonToBuffer({
+ 'name': 'tinymce/tinymce',
+ 'version': packageData.version,
+ 'description': 'Web based JavaScript HTML WYSIWYG editor control.',
+ 'license': ['LGPL-2.1-only'],
+ 'keywords': ['editor', 'wysiwyg', 'tinymce', 'richtext', 'javascript', 'html'],
+ 'homepage': 'http://www.tinymce.com',
+ 'type': 'component',
+ 'extra': {
+ 'component': {
+ 'scripts': [
+ 'tinymce.js',
+ 'plugins/*/plugin.js',
+ 'themes/*/theme.js'
+ ],
+ 'files': [
+ 'tinymce.min.js',
+ 'plugins/*/plugin.min.js',
+ 'themes/*/theme.min.js',
+ 'skins/**'
+ ]
+ }
+ },
+ 'archive': {
+ 'exclude': ['readme.md', 'bower.js', 'package.json', '.npmignore', 'changelog.txt']
+ }
+ }));
+
+ zip.addFile(
+ 'jquery.tinymce.js',
+ 'js/tinymce/jquery.tinymce.min.js'
+ );
+
+ var getDirs = zipUtils.getDirectories(grunt, this.excludes);
+
+ zipUtils.addIndexFiles(
+ zip,
+ getDirs('js/tinymce/plugins'),
+ zipUtils.generateIndex('plugins', 'plugin')
+ );
+ zipUtils.addIndexFiles(
+ zip,
+ getDirs('js/tinymce/themes'),
+ zipUtils.generateIndex('themes', 'theme')
+ );
+ },
+ to: 'tmp/tinymce_<%= pkg.version %>_component.zip'
+ },
+ src: [
+ 'js/tinymce/skins',
+ 'js/tinymce/plugins',
+ 'js/tinymce/themes',
+ 'js/tinymce/tinymce.js',
+ 'js/tinymce/tinymce.min.js',
+ 'js/tinymce/jquery.tinymce.min.js',
+ 'js/tinymce/license.txt',
+ 'changelog.txt',
+ 'readme.md'
+ ]
+ }
+ },
+
+ nugetpack: {
+ main: {
+ options: {
+ id: 'TinyMCE',
+ version: packageData.version,
+ authors: 'Ephox Corp',
+ owners: 'Ephox Corp',
+ description: 'The best WYSIWYG editor! TinyMCE is a platform independent web based Javascript HTML WYSIWYG editor ' +
+ 'control released as Open Source under LGPL by Ephox Corp. TinyMCE has the ability to convert HTML ' +
+ 'TEXTAREA fields or other HTML elements to editor instances. TinyMCE is very easy to integrate ' +
+ 'into other Content Management Systems.',
+ releaseNotes: 'Release notes for my package.',
+ summary: 'TinyMCE is a platform independent web based Javascript HTML WYSIWYG editor ' +
+ 'control released as Open Source under LGPL by Ephox Corp.',
+ projectUrl: 'http://www.tinymce.com/',
+ iconUrl: 'http://www.tinymce.com/favicon.ico',
+ licenseUrl: 'http://www.tinymce.com/license',
+ requireLicenseAcceptance: true,
+ tags: 'Editor TinyMCE HTML HTMLEditor',
+ excludes: [
+ 'js/**/config',
+ 'js/**/scratch',
+ 'js/**/classes',
+ 'js/**/lib',
+ 'js/**/dependency',
+ 'js/**/src',
+ 'js/**/*.less',
+ 'js/**/*.dev.svg',
+ 'js/**/*.dev.js',
+ 'js/**/*.map',
+ 'js/tinymce/tinymce.full.min.js'
+ ],
+ outputDir: 'tmp'
+ },
+ files: [
+ { src: 'js/tinymce/langs', dest: '/content/scripts/tinymce/langs' },
+ { src: 'js/tinymce/plugins', dest: '/content/scripts/tinymce/plugins' },
+ { src: 'js/tinymce/themes', dest: '/content/scripts/tinymce/themes' },
+ { src: 'js/tinymce/skins', dest: '/content/scripts/tinymce/skins' },
+ { src: 'js/tinymce/tinymce.js', dest: '/content/scripts/tinymce/tinymce.js' },
+ { src: 'js/tinymce/tinymce.min.js', dest: '/content/scripts/tinymce/tinymce.min.js' },
+ { src: 'js/tinymce/jquery.tinymce.min.js', dest: '/content/scripts/tinymce/jquery.tinymce.min.js' },
+ { src: 'js/tinymce/license.txt', dest: '/content/scripts/tinymce/license.txt' }
+ ]
+ },
+
+ jquery: {
+ options: {
+ id: 'TinyMCE.jQuery',
+ title: 'TinyMCE.jQuery [Deprecated]',
+ version: packageData.version,
+ authors: 'Ephox Corp',
+ owners: 'Ephox Corp',
+ description: 'This package has been deprecated use https://www.nuget.org/packages/TinyMCE/',
+ releaseNotes: 'This package has been deprecated use https://www.nuget.org/packages/TinyMCE/',
+ summary: 'This package has been deprecated use https://www.nuget.org/packages/TinyMCE/',
+ projectUrl: 'http://www.tinymce.com/',
+ iconUrl: 'http://www.tinymce.com/favicon.ico',
+ licenseUrl: 'http://www.tinymce.com/license',
+ requireLicenseAcceptance: true,
+ tags: 'Editor TinyMCE HTML HTMLEditor',
+ excludes: [
+ 'js/**/config',
+ 'js/**/scratch',
+ 'js/**/classes',
+ 'js/**/lib',
+ 'js/**/dependency',
+ 'js/**/src',
+ 'js/**/*.less',
+ 'js/**/*.dev.svg',
+ 'js/**/*.dev.js',
+ 'js/**/*.map',
+ 'js/tinymce/tinymce.full.min.js'
+ ],
+ outputDir: 'tmp'
+ },
+
+ files: [
+ { src: 'js/tinymce/langs', dest: '/content/scripts/tinymce/langs' },
+ { src: 'js/tinymce/plugins', dest: '/content/scripts/tinymce/plugins' },
+ { src: 'js/tinymce/themes', dest: '/content/scripts/tinymce/themes' },
+ { src: 'js/tinymce/skins', dest: '/content/scripts/tinymce/skins' },
+ { src: 'js/tinymce/tinymce.js', dest: '/content/scripts/tinymce/tinymce.js' },
+ { src: 'js/tinymce/tinymce.min.js', dest: '/content/scripts/tinymce/tinymce.min.js' },
+ { src: 'js/tinymce/jquery.tinymce.min.js', dest: '/content/scripts/tinymce/jquery.tinymce.min.js' },
+ { src: 'js/tinymce/license.txt', dest: '/content/scripts/tinymce/license.txt' }
+ ]
+ }
+ },
+
+ bundle: {
+ minified: {
+ options: {
+ themesDir: 'js/tinymce/themes',
+ pluginsDir: 'js/tinymce/plugins',
+ pluginFileName: 'plugin.min.js',
+ themeFileName: 'theme.min.js',
+ outputPath: 'js/tinymce/tinymce.full.min.js'
+ },
+
+ src: [
+ 'js/tinymce/tinymce.min.js'
+ ]
+ },
+
+ source: {
+ options: {
+ themesDir: 'js/tinymce/themes',
+ pluginsDir: 'js/tinymce/plugins',
+ pluginFileName: 'plugin.js',
+ themeFileName: 'theme.js',
+ outputPath: 'js/tinymce/tinymce.full.js'
+ },
+
+ src: [
+ 'js/tinymce/tinymce.js'
+ ]
+ }
+ },
+
+ clean: {
+ dist: ['js'],
+ lib: ['lib'],
+ scratch: ['scratch'],
+ release: ['tmp']
+ },
+
+ 'bedrock-manual': {
+ core: {
+ config: 'tsconfig.json',
+ projectdir: '.',
+ stopOnFailure: true,
+ testfiles: [
+ 'src/**/test/ts/atomic/**/*Test.ts',
+ 'src/**/test/ts/browser/**/*Test.ts'
+ ],
+ customRoutes: 'src/core/test/json/routes.json'
+ }
+ },
+
+ 'bedrock-auto': {
+ phantomjs: {
+ browser: 'phantomjs',
+ config: 'tsconfig.json',
+ testfiles: ['src/**/test/ts/**/*Test.ts'],
+ stopOnFailure: true,
+ overallTimeout: 600000,
+ singleTimeout: 300000,
+ customRoutes: 'src/core/test/json/routes.json',
+ name: 'phantomjs'
+ },
+ 'chrome-headless': {
+ browser: 'chrome-headless',
+ config: 'tsconfig.json',
+ testfiles: ['src/**/test/ts/**/*Test.ts'],
+ stopOnFailure: true,
+ overallTimeout: 600000,
+ singleTimeout: 300000,
+ customRoutes: 'src/core/test/json/routes.json',
+ name: 'chrome-headless'
+ },
+ chrome: {
+ browser: 'chrome',
+ config: 'tsconfig.json',
+ testfiles: ['src/**/test/ts/**/*Test.ts'],
+ stopOnFailure: true,
+ overallTimeout: 600000,
+ singleTimeout: 300000,
+ customRoutes: 'src/core/test/json/routes.json',
+ name: 'chrome'
+ },
+ firefox: {
+ browser: 'firefox',
+ config: 'tsconfig.json',
+ testfiles: ['src/**/test/ts/**/*Test.ts'],
+ stopOnFailure: true,
+ overallTimeout: 600000,
+ singleTimeout: 300000,
+ customRoutes: 'src/core/test/json/routes.json',
+ name: 'firefox'
+ },
+ MicrosoftEdge: {
+ browser: 'MicrosoftEdge',
+ config: 'tsconfig.json',
+ testfiles: ['src/**/test/ts/**/*Test.ts'],
+ stopOnFailure: true,
+ overallTimeout: 600000,
+ singleTimeout: 300000,
+ customRoutes: 'src/core/test/json/routes.json',
+ name: 'MicrosoftEdge'
+ },
+ ie: {
+ browser: 'ie',
+ config: 'tsconfig.json',
+ testfiles: ['src/**/test/ts/**/*Test.ts'],
+ stopOnFailure: true,
+ overallTimeout: 600000,
+ singleTimeout: 300000,
+ customRoutes: 'src/core/test/json/routes.json',
+ name: 'ie'
+ }
+ },
+
+ watch: {
+ skins: {
+ files: ['src/skins/lightgray/main/less/**/*'],
+ tasks: ['less', 'copy:skins'],
+ options: {
+ spawn: false
+ }
+ }
+ }
+ });
+
+ grunt.registerTask('version', 'Creates a version file', function () {
+ grunt.file.write('tmp/version.txt', BUILD_VERSION);
+ });
+
+ grunt.registerTask('build-headers', 'Appends build headers to js files', function () {
+ var header = '// ' + packageData.version + ' (' + packageData.date + ')\n';
+ grunt.file.write('js/tinymce/tinymce.js', header + grunt.file.read('js/tinymce/tinymce.js'));
+ grunt.file.write('js/tinymce/tinymce.min.js', header + grunt.file.read('js/tinymce/tinymce.min.js'));
+ });
+
+ require('load-grunt-tasks')(grunt);
+ grunt.loadTasks('tools/tasks');
+ grunt.loadNpmTasks('@ephox/bedrock');
+ grunt.loadNpmTasks('@ephox/swag');
+ grunt.loadNpmTasks('grunt-tslint');
+
+ grunt.registerTask('prod', [
+ 'validateVersion',
+ 'shell:tsc',
+ 'tslint',
+ 'globals',
+ 'rollup',
+ 'uglify',
+ 'less',
+ 'copy',
+ 'build-headers',
+ 'clean:release',
+ 'moxiezip',
+ 'nugetpack',
+ 'version'
+ ]);
+
+ grunt.registerTask('dev', [
+ 'shell:tsc',
+ 'globals',
+ 'rollup',
+ 'less',
+ 'copy'
+ ]);
+
+ grunt.registerTask('start', ['webpack-dev-server']);
+
+ grunt.registerTask('default', ['prod']);
+ grunt.registerTask('test', ['bedrock-auto:phantomjs']);
+};
diff --git a/tools-ng/tinymce/editor/LICENSE.TXT b/tools-ng/tinymce/editor/LICENSE.TXT
new file mode 100644
index 000000000..b17fc9049
--- /dev/null
+++ b/tools-ng/tinymce/editor/LICENSE.TXT
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ annotations demo Page
+ commands demo Page
+ Tinymce content editable false Page
+ Tinymce custom theme Page
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/src/core/demo/html/full_demo.html b/tools-ng/tinymce/editor/src/core/demo/html/full_demo.html
new file mode 100644
index 000000000..25f8b65b8
--- /dev/null
+++ b/tools-ng/tinymce/editor/src/core/demo/html/full_demo.html
@@ -0,0 +1,17 @@
+
+
+
+ Tinymce full featured Page
+ jQuery integration Demo Page
+ TinyMCE Source Dump Demo Page
+
+ TinyMCE Demo Page
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/src/core/demo/html/ui_container.html b/tools-ng/tinymce/editor/src/core/demo/html/ui_container.html
new file mode 100644
index 000000000..de0b5e172
--- /dev/null
+++ b/tools-ng/tinymce/editor/src/core/demo/html/ui_container.html
@@ -0,0 +1,81 @@
+
+
+
+
+ Left side iframe
+
+
+ Left side inline
+
+
+
+
+
+
+
+
+
+
+
+
+ Right side iframe
+
+
+ Right side inline
+
+
+
+
+
+
+
+
+
+
+
+
+
a
b
c
+ * + * @example + * var parser = new tinymce.html.DomParser({validate: true}, schema); + * var rootNode = parser.parse('x
->x
+ const trim = function (rootBlockNode) { + if (rootBlockNode) { + node = rootBlockNode.firstChild; + if (node && node.type === 3) { + node.value = node.value.replace(startWhiteSpaceRegExp, ''); + } + + node = rootBlockNode.lastChild; + if (node && node.type === 3) { + node.value = node.value.replace(endWhiteSpaceRegExp, ''); + } + } + }; + + // Check if rootBlock is valid within rootNode for example if P is valid in H1 if H1 is the contentEditabe root + if (!schema.isValidChild(rootNode.name, rootBlockName.toLowerCase())) { + return; + } + + while (node) { + next = node.next; + + if (node.type === 3 || (node.type === 1 && node.name !== 'p' && + !blockElements[node.name] && !node.attr('data-mce-type'))) { + if (!rootBlockNode) { + // Create a new root block element + rootBlockNode = createNode(rootBlockName, 1); + rootBlockNode.attr(settings.forced_root_block_attrs); + rootNode.insert(rootBlockNode, node); + rootBlockNode.append(node); + } else { + rootBlockNode.append(node); + } + } else { + trim(rootBlockNode); + rootBlockNode = null; + } + + node = next; + } + + trim(rootBlockNode); + }; + + const createNode = function (name, type) { + const node = new Node(name, type); + let list; + + if (name in nodeFilters) { + list = matchedNodes[name]; + + if (list) { + list.push(node); + } else { + matchedNodes[name] = [node]; + } + } + + return node; + }; + + const removeWhitespaceBefore = function (node) { + let textNode, textNodeNext, textVal, sibling; + const blockElements = schema.getBlockElements(); + + for (textNode = node.prev; textNode && textNode.type === 3;) { + textVal = textNode.value.replace(endWhiteSpaceRegExp, ''); + + // Found a text node with non whitespace then trim that and break + if (textVal.length > 0) { + textNode.value = textVal; + return; + } + + textNodeNext = textNode.next; + + // Fix for bug #7543 where bogus nodes would produce empty + // text nodes and these would be removed if a nested list was before it + if (textNodeNext) { + if (textNodeNext.type === 3 && textNodeNext.value.length) { + textNode = textNode.prev; + continue; + } + + if (!blockElements[textNodeNext.name] && textNodeNext.name !== 'script' && textNodeNext.name !== 'style') { + textNode = textNode.prev; + continue; + } + } + + sibling = textNode.prev; + textNode.remove(); + textNode = sibling; + } + }; + + const cloneAndExcludeBlocks = function (input) { + let name; + const output = {}; + + for (name in input) { + if (name !== 'li' && name !== 'p') { + output[name] = input[name]; + } + } + + return output; + }; + + parser = SaxParser({ + validate, + allow_script_urls: settings.allow_script_urls, + allow_conditional_comments: settings.allow_conditional_comments, + + // Exclude P and LI from DOM parsing since it's treated better by the DOM parser + self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), + + cdata (text) { + node.append(createNode('#cdata', 4)).value = text; + }, + + text (text, raw) { + let textNode; + + // Trim all redundant whitespace on non white space elements + if (!isInWhiteSpacePreservedElement) { + text = text.replace(allWhiteSpaceRegExp, ' '); + + if (isLineBreakNode(node.lastChild, blockElements)) { + text = text.replace(startWhiteSpaceRegExp, ''); + } + } + + // Do we need to create the node + if (text.length !== 0) { + textNode = createNode('#text', 3); + textNode.raw = !!raw; + node.append(textNode).value = text; + } + }, + + comment (text) { + node.append(createNode('#comment', 8)).value = text; + }, + + pi (name, text) { + node.append(createNode(name, 7)).value = text; + removeWhitespaceBefore(node); + }, + + doctype (text) { + let newNode; + + newNode = node.append(createNode('#doctype', 10)); + newNode.value = text; + removeWhitespaceBefore(node); + }, + + start (name, attrs, empty) { + let newNode, attrFiltersLen, elementRule, attrName, parent; + + elementRule = validate ? schema.getElementRule(name) : {}; + if (elementRule) { + newNode = createNode(elementRule.outputName || name, 1); + newNode.attributes = attrs; + newNode.shortEnded = empty; + + node.append(newNode); + + // Check if node is valid child of the parent node is the child is + // unknown we don't collect it since it's probably a custom element + parent = children[node.name]; + if (parent && children[newNode.name] && !parent[newNode.name]) { + invalidChildren.push(newNode); + } + + attrFiltersLen = attributeFilters.length; + while (attrFiltersLen--) { + attrName = attributeFilters[attrFiltersLen].name; + + if (attrName in attrs.map) { + list = matchedAttributes[attrName]; + + if (list) { + list.push(newNode); + } else { + matchedAttributes[attrName] = [newNode]; + } + } + } + + // Trim whitespace before block + if (blockElements[name]) { + removeWhitespaceBefore(newNode); + } + + // Change current node if the element wasn't empty i.e nottext
')); + * @class tinymce.html.Serializer + * @version 3.4 + */ + +export default function (settings?, schema = Schema()) { + const writer = Writer(settings); + + settings = settings || {}; + settings.validate = 'validate' in settings ? settings.validate : true; + + /** + * Serializes the specified node into a string. + * + * @example + * new tinymce.html.Serializer().serialize(new tinymce.html.DomParser().parse('text
')); + * @method serialize + * @param {tinymce.html.Node} node Node instance to serialize. + * @return {String} String with HTML based on DOM tree. + */ + const serialize = (node: Node) => { + let handlers, validate; + + validate = settings.validate; + + handlers = { + // #text + 3 (node) { + writer.text(node.value, node.raw); + }, + + // #comment + 8 (node) { + writer.comment(node.value); + }, + + // Processing instruction + 7 (node) { + writer.pi(node.name, node.value); + }, + + // Doctype + 10 (node) { + writer.doctype(node.value); + }, + + // CDATA + 4 (node) { + writer.cdata(node.value); + }, + + // Document fragment + 11 (node) { + if ((node = node.firstChild)) { + do { + walk(node); + } while ((node = node.next)); + } + } + }; + + writer.reset(); + + const walk = function (node: Node) { + const handler = handlers[node.type]; + let name, isEmpty, attrs, attrName, attrValue, sortedAttrs, i, l, elementRule; + + if (!handler) { + name = node.name; + isEmpty = node.shortEnded; + attrs = node.attributes; + + // Sort attributes + if (validate && attrs && attrs.length > 1) { + sortedAttrs = []; + sortedAttrs.map = {}; + + elementRule = schema.getElementRule(node.name); + if (elementRule) { + for (i = 0, l = elementRule.attributesOrder.length; i < l; i++) { + attrName = elementRule.attributesOrder[i]; + + if (attrName in attrs.map) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({ name: attrName, value: attrValue }); + } + } + + for (i = 0, l = attrs.length; i < l; i++) { + attrName = attrs[i].name; + + if (!(attrName in sortedAttrs.map)) { + attrValue = attrs.map[attrName]; + sortedAttrs.map[attrName] = attrValue; + sortedAttrs.push({ name: attrName, value: attrValue }); + } + } + + attrs = sortedAttrs; + } + } + + writer.start(node.name, attrs, isEmpty); + + if (!isEmpty) { + if ((node = node.firstChild)) { + do { + walk(node); + } while ((node = node.next)); + } + + writer.end(name); + } + } else { + handler(node); + } + }; + + // Serialize element and treat all non elements as fragments + if (node.type === 1 && !settings.inner) { + walk(node); + } else { + handlers[11](node); + } + + return writer.getContent(); + }; + + return { + serialize + }; +} \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/api/html/Styles.ts b/tools-ng/tinymce/editor/src/core/main/ts/api/html/Styles.ts new file mode 100644 index 000000000..af64a30b9 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/api/html/Styles.ts @@ -0,0 +1,387 @@ +/** + * Styles.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 + */ + +/** + * This class is used to parse CSS styles it also compresses styles to reduce the output size. + * + * @example + * var Styles = new tinymce.html.Styles({ + * url_converter: function(url) { + * return url; + * } + * }); + * + * styles = Styles.parse('border: 1px solid red'); + * styles.color = 'red'; + * + * console.log(new tinymce.html.StyleSerializer().serialize(styles)); + * + * @class tinymce.html.Styles + * @version 3.4 + */ + +import Schema from './Schema'; + +export interface StyleMap { [s: string]: string | number; } +export interface Styles { + toHex(color: string): string; + parse(css: string): StyleMap; + serialize(styles: StyleMap, elementName?: string): string; +} + +const toHex = (match: string, r: string, g: string, b: string) => { + const hex = (val: string) => { + val = parseInt(val, 10).toString(16); + + return val.length > 1 ? val : '0' + val; // 0 -> 00 + }; + + return '#' + hex(r) + hex(g) + hex(b); +}; + +export function Styles(settings?, schema?: Schema): Styles { + /*jshint maxlen:255 */ + /*eslint max-len:0 */ + const rgbRegExp = /rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi; + const urlOrStrRegExp = /(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi; + const styleRegExp = /\s*([^:]+):\s*([^;]+);?/g; + const trimRightRegExp = /\s+$/; + let i; + const encodingLookup = {}; + let encodingItems; + let validStyles; + let invalidStyles; + const invisibleChar = '\uFEFF'; + + settings = settings || {}; + + if (schema) { + validStyles = schema.getValidStyles(); + invalidStyles = schema.getInvalidStyles(); + } + + encodingItems = ('\\" \\\' \\; \\: ; : ' + invisibleChar).split(' '); + for (i = 0; i < encodingItems.length; i++) { + encodingLookup[encodingItems[i]] = invisibleChar + i; + encodingLookup[invisibleChar + i] = encodingItems[i]; + } + + return { + /** + * Parses the specified RGB color value and returns a hex version of that color. + * + * @method toHex + * @param {String} color RGB string value like rgb(1,2,3) + * @return {String} Hex version of that RGB value like #FF00FF. + */ + toHex (color: string): string { + return color.replace(rgbRegExp, toHex); + }, + + /** + * Parses the specified style value into an object collection. This parser will also + * merge and remove any redundant items that browsers might have added. It will also convert non hex + * colors to hex values. Urls inside the styles will also be converted to absolute/relative based on settings. + * + * @method parse + * @param {String} css Style value to parse for example: border:1px solid red;. + * @return {Object} Object representation of that style like {border: '1px solid red'} + */ + parse (css: string): StyleMap { + const styles: any = {}; + let matches, name, value, isEncoded; + const urlConverter = settings.url_converter; + const urlConverterScope = settings.url_converter_scope || this; + + const compress = function (prefix, suffix, noJoin?) { + let top, right, bottom, left; + + top = styles[prefix + '-top' + suffix]; + if (!top) { + return; + } + + right = styles[prefix + '-right' + suffix]; + if (!right) { + return; + } + + bottom = styles[prefix + '-bottom' + suffix]; + if (!bottom) { + return; + } + + left = styles[prefix + '-left' + suffix]; + if (!left) { + return; + } + + const box = [top, right, bottom, left]; + i = box.length - 1; + while (i--) { + if (box[i] !== box[i + 1]) { + break; + } + } + + if (i > -1 && noJoin) { + return; + } + + styles[prefix + suffix] = i === -1 ? box[0] : box.join(' '); + delete styles[prefix + '-top' + suffix]; + delete styles[prefix + '-right' + suffix]; + delete styles[prefix + '-bottom' + suffix]; + delete styles[prefix + '-left' + suffix]; + }; + + /** + * Checks if the specific style can be compressed in other words if all border-width are equal. + */ + const canCompress = function (key) { + let value = styles[key], i; + + if (!value) { + return; + } + + value = value.split(' '); + i = value.length; + while (i--) { + if (value[i] !== value[0]) { + return false; + } + } + + styles[key] = value[0]; + + return true; + }; + + /** + * Compresses multiple styles into one style. + */ + const compress2 = function (target, a, b, c) { + if (!canCompress(a)) { + return; + } + + if (!canCompress(b)) { + return; + } + + if (!canCompress(c)) { + return; + } + + // Compress + styles[target] = styles[a] + ' ' + styles[b] + ' ' + styles[c]; + delete styles[a]; + delete styles[b]; + delete styles[c]; + }; + + // Encodes the specified string by replacing all \" \' ; : with _.
+ *
+ * @method start
+ * @param {String} name Name of the element.
+ * @param {Array} attrs Optional attribute array or undefined if it hasn't any.
+ * @param {Boolean} empty Optional empty state if the tag should end like
.
+ */
+ start (name: string, attrs?: Attributes, empty?: boolean) {
+ let i, l, attr, value;
+
+ if (indent && indentBefore[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+
+ html.push('<', name);
+
+ if (attrs) {
+ for (i = 0, l = attrs.length; i < l; i++) {
+ attr = attrs[i];
+ html.push(' ', attr.name, '="', encode(attr.value, true), '"');
+ }
+ }
+
+ if (!empty || htmlOutput) {
+ html[html.length] = '>';
+ } else {
+ html[html.length] = ' />';
+ }
+
+ if (empty && indent && indentAfter[name] && html.length > 0) {
+ value = html[html.length - 1];
+
+ if (value.length > 0 && value !== '\n') {
+ html.push('\n');
+ }
+ }
+ },
+
+ /**
+ * Writes the a end element such as
a|c
+ * p[0]/img[0],before =|
|
a|b
->a|b
+const fromPosition = function (forward: boolean, root: Node, pos: CaretPosition) { + const walker = CaretWalker(root); + return Option.from(forward ? walker.next(pos) : walker.prev(pos)); +}; + +// Finds:a|b
->ab|
+const navigate = (forward: boolean, root: Element, from: CaretPosition) => { + return fromPosition(forward, root, from).bind(function (to) { + if (CaretUtils.isInSameBlock(from, to, root) && shouldSkipPosition(forward, from, to)) { + return fromPosition(forward, root, to); + } else { + return Option.some(to); + } + }); +}; + +const positionIn = (forward: boolean, element: Element): Optiona
|b
|
a
->|a
+const isBrBeforeBlock = (node: Node, root: Node): boolean => { + let next; + + if (!NodeType.isBr(node)) { + return false; + } + + // Handles the casea|
b
a
|b
|
+ rng = selection.getRng(); + const caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null); + const body = editor.getBody(); + if (caretElement === body && selection.isCollapsed()) { + if (dom.isBlock(body.firstChild) && canHaveChildren(editor, body.firstChild) && dom.isEmpty(body.firstChild)) { + rng = dom.createRng(); + rng.setStart(body.firstChild, 0); + rng.setEnd(body.firstChild, 0); + selection.setRng(rng); + } + } + + // Insert node maker where we will insert the new HTML and get it's parent + if (!selection.isCollapsed()) { + // Fix for #2595 seems that delete removes one extra character on + // WebKit for some odd reason if you double click select a word + editor.selection.setRng(RangeNormalizer.normalize(editor.selection.getRng())); + editor.getDoc().execCommand('Delete', false, null); + value = trimNbspAfterDeleteAndPadValue(editor.selection.getRng(), value); + } + + parentNode = selection.getNode(); + + // Parse the fragment within the context of the parent node + const parserArgs: any = { context: parentNode.nodeName.toLowerCase(), data: details.data, insert: true }; + fragment = parser.parse(value, parserArgs); + + // Custom handling of lists + if (details.paste === true && InsertList.isListFragment(editor.schema, fragment) && InsertList.isParentBlockLi(dom, parentNode)) { + rng = InsertList.insertAtCaret(serializer, dom, editor.selection.getRng(), fragment); + editor.selection.setRng(rng); + editor.fire('SetContent', args); + return; + } + + markFragmentElements(fragment); + + // Move the caret to a more suitable location + node = fragment.lastChild; + if (node.attr('id') === 'mce_marker') { + marker = node; + + for (node = node.prev; node; node = node.walk(true)) { + if (node.type === 3 || !dom.isBlock(node.name)) { + if (editor.schema.isValidChild(node.parent.name, 'span')) { + node.parent.insert(marker, node, node.name === 'br'); + } + break; + } + } + } + + editor._selectionOverrides.showBlockCaretContainer(parentNode); + + // If parser says valid we can insert the contents into that parent + if (!parserArgs.invalid) { + value = serializer.serialize(fragment); + validInsertion(editor, value, parentNode); + } else { + // If the fragment was invalid within that context then we need + // to parse and process the parent it's inserted into + + // Insert bookmark node and get the parent + selectionSetContent(editor, bookmarkHtml); + parentNode = selection.getNode(); + rootNode = editor.getBody(); + + // Opera will return the document node when selection is in root + if (parentNode.nodeType === 9) { + parentNode = node = rootNode; + } else { + node = parentNode; + } + + // Find the ancestor just before the root element + while (node !== rootNode) { + parentNode = node; + node = node.parentNode; + } + + // Get the outer/inner HTML depending on if we are in the root and parser and serialize that + value = parentNode === rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode); + value = serializer.serialize( + parser.parse( + // Need to replace by using a function since $ in the contents would otherwise be a problem + value.replace(//i, function () { + return serializer.serialize(fragment); + }) + ) + ); + + // Set the inner/outer HTML depending on if we are in the root or not + if (parentNode === rootNode) { + dom.setHTML(rootNode, value); + } else { + dom.setOuterHTML(parentNode, value); + } + } + + reduceInlineTextElements(editor, merge); + moveSelectionToMarker(editor, dom.get('mce_marker')); + umarkFragmentElements(editor.getBody()); + trimBrsFromTableCell(editor.dom, editor.selection.getStart()); + + editor.fire('SetContent', args); + editor.addVisual(); +}; + +const processValue = function (value) { + let details; + + if (typeof value !== 'string') { + details = Tools.extend({ + paste: value.paste, + data: { + paste: value.paste + } + }, value); + + return { + content: value.content, + details + }; + } + + return { + content: value, + details: {} + }; +}; + +const insertAtCaret = function (editor, value) { + const result = processValue(value); + insertHtmlAtCaret(editor, result.content, result.details); +}; + +export default { + insertAtCaret +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/content/InsertList.ts b/tools-ng/tinymce/editor/src/core/main/ts/content/InsertList.ts new file mode 100644 index 000000000..0140fefc0 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/content/InsertList.ts @@ -0,0 +1,210 @@ +/** + * InsertList.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 + */ + +import CaretPosition from '../caret/CaretPosition'; +import { CaretWalker } from '../caret/CaretWalker'; +import NodeType from '../dom/NodeType'; +import Tools from '../api/util/Tools'; +import { Range } from '@ephox/dom-globals'; + +/** + * Handles inserts of lists into the editor instance. + * + * @class tinymce.InsertList + * @private + */ + +const hasOnlyOneChild = function (node) { + return node.firstChild && node.firstChild === node.lastChild; +}; + +const isPaddingNode = function (node) { + return node.name === 'br' || node.value === '\u00a0'; +}; + +const isPaddedEmptyBlock = function (schema, node) { + const blockElements = schema.getBlockElements(); + return blockElements[node.name] && hasOnlyOneChild(node) && isPaddingNode(node.firstChild); +}; + +const isEmptyFragmentElement = function (schema, node) { + const nonEmptyElements = schema.getNonEmptyElements(); + return node && (node.isEmpty(nonEmptyElements) || isPaddedEmptyBlock(schema, node)); +}; + +const isListFragment = function (schema, fragment) { + let firstChild = fragment.firstChild; + let lastChild = fragment.lastChild; + + // Skip meta since it's likelya b
->a &nsbp;b
ora
->a
|a when there is only one item left except the zwsp caret container nodes
+const hasOnlyTwoOrLessPositionsLeft = function (elm) {
+ return Options.liftN([
+ CaretFinder.firstPositionIn(elm),
+ CaretFinder.lastPositionIn(elm)
+ ], function (firstPos, lastPos) {
+ const normalizedFirstPos = InlineUtils.normalizePosition(true, firstPos);
+ const normalizedLastPos = InlineUtils.normalizePosition(false, lastPos);
+
+ return CaretFinder.nextPosition(elm, normalizedFirstPos).map(function (pos) {
+ return pos.isEqual(normalizedLastPos);
+ }).getOr(true);
+ }).getOr(true);
+};
+
+const setCaretLocation = function (editor, caret) {
+ return function (location) {
+ return BoundaryCaret.renderCaret(caret, location).map(function (pos) {
+ BoundarySelection.setCaretPosition(editor, pos);
+ return true;
+ }).getOr(false);
+ };
+};
+
+const deleteFromTo = function (editor, caret, from, to) {
+ const rootNode = editor.getBody();
+ const isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
+
+ editor.undoManager.ignore(function () {
+ editor.selection.setRng(rangeFromPositions(from, to));
+ editor.execCommand('Delete');
+
+ BoundaryLocation.readLocation(isInlineTarget, rootNode, CaretPosition.fromRangeStart(editor.selection.getRng()))
+ .map(BoundaryLocation.inside)
+ .map(setCaretLocation(editor, caret));
+ });
+
+ editor.nodeChanged();
+};
+
+const rescope = function (rootNode, node) {
+ const parentBlock = CaretUtils.getParentBlock(node, rootNode);
+ return parentBlock ? parentBlock : rootNode;
+};
+
+const backspaceDeleteCollapsed = function (editor, caret, forward, from) {
+ const rootNode = rescope(editor.getBody(), from.container());
+ const isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor);
+ const fromLocation = BoundaryLocation.readLocation(isInlineTarget, rootNode, from);
+
+ return fromLocation.bind(function (location) {
+ if (forward) {
+ return location.fold(
+ Fun.constant(Option.some(BoundaryLocation.inside(location))), // Before
+ Option.none, // Start
+ Fun.constant(Option.some(BoundaryLocation.outside(location))), // End
+ Option.none // After
+ );
+ } else {
+ return location.fold(
+ Option.none, // Before
+ Fun.constant(Option.some(BoundaryLocation.outside(location))), // Start
+ Option.none, // End
+ Fun.constant(Option.some(BoundaryLocation.inside(location))) // After
+ );
+ }
+ })
+ .map(setCaretLocation(editor, caret))
+ .getOrThunk(function () {
+ const toPosition = CaretFinder.navigate(forward, rootNode, from);
+ const toLocation = toPosition.bind(function (pos) {
+ return BoundaryLocation.readLocation(isInlineTarget, rootNode, pos);
+ });
+
+ if (fromLocation.isSome() && toLocation.isSome()) {
+ return InlineUtils.findRootInline(isInlineTarget, rootNode, from).map(function (elm) {
+ if (hasOnlyTwoOrLessPositionsLeft(elm)) {
+ DeleteElement.deleteElement(editor, forward, Element.fromDom(elm));
+ return true;
+ } else {
+ return false;
+ }
+ }).getOr(false);
+ } else {
+ return toLocation.bind(function (_) {
+ return toPosition.map(function (to) {
+ if (forward) {
+ deleteFromTo(editor, caret, from, to);
+ } else {
+ deleteFromTo(editor, caret, to, from);
+ }
+
+ return true;
+ });
+ }).getOr(false);
+ }
+ });
+};
+
+const backspaceDelete = function (editor, caret, forward?) {
+ if (editor.selection.isCollapsed() && isFeatureEnabled(editor)) {
+ const from = CaretPosition.fromRangeStart(editor.selection.getRng());
+ return backspaceDeleteCollapsed(editor, caret, forward, from);
+ }
+
+ return false;
+};
+
+export default {
+ backspaceDelete
+};
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/src/core/main/ts/delete/InlineFormatDelete.ts b/tools-ng/tinymce/editor/src/core/main/ts/delete/InlineFormatDelete.ts
new file mode 100644
index 000000000..709d3da3a
--- /dev/null
+++ b/tools-ng/tinymce/editor/src/core/main/ts/delete/InlineFormatDelete.ts
@@ -0,0 +1,70 @@
+/**
+ * InlineFormatDelete.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
+ */
+
+import { Arr, Fun } from '@ephox/katamari';
+import { Element, Traverse } from '@ephox/sugar';
+import CaretPosition from '../caret/CaretPosition';
+import DeleteElement from './DeleteElement';
+import DeleteUtils from './DeleteUtils';
+import * as ElementType from '../dom/ElementType';
+import Parents from '../dom/Parents';
+import * as CaretFormat from '../fmt/CaretFormat';
+
+const getParentInlines = function (rootElm, startElm) {
+ const parents = Parents.parentsAndSelf(startElm, rootElm);
+ return Arr.findIndex(parents, ElementType.isBlock).fold(
+ Fun.constant(parents),
+ function (index) {
+ return parents.slice(0, index);
+ }
+ );
+};
+
+const hasOnlyOneChild = function (elm) {
+ return Traverse.children(elm).length === 1;
+};
+
+const deleteLastPosition = function (forward, editor, target, parentInlines) {
+ const isFormatElement = Fun.curry(CaretFormat.isFormatElement, editor);
+ const formatNodes = Arr.map(Arr.filter(parentInlines, isFormatElement), function (elm) {
+ return elm.dom();
+ });
+
+ if (formatNodes.length === 0) {
+ DeleteElement.deleteElement(editor, forward, target);
+ } else {
+ const pos = CaretFormat.replaceWithCaretFormat(target.dom(), formatNodes);
+ editor.selection.setRng(pos.toRange());
+ }
+};
+
+const deleteCaret = function (editor, forward) {
+ const rootElm = Element.fromDom(editor.getBody());
+ const startElm = Element.fromDom(editor.selection.getStart());
+ const parentInlines = Arr.filter(getParentInlines(rootElm, startElm), hasOnlyOneChild);
+
+ return Arr.last(parentInlines).map(function (target) {
+ const fromPos = CaretPosition.fromRangeStart(editor.selection.getRng());
+ if (DeleteUtils.willDeleteLastPositionInElement(forward, fromPos, target.dom()) && !CaretFormat.isEmptyCaretFormatElement(target)) {
+ deleteLastPosition(forward, editor, target, parentInlines);
+ return true;
+ } else {
+ return false;
+ }
+ }).getOr(false);
+};
+
+const backspaceDelete = function (editor, forward) {
+ return editor.selection.isCollapsed() ? deleteCaret(editor, forward) : false;
+};
+
+export default {
+ backspaceDelete
+};
\ No newline at end of file
diff --git a/tools-ng/tinymce/editor/src/core/main/ts/delete/MergeBlocks.ts b/tools-ng/tinymce/editor/src/core/main/ts/delete/MergeBlocks.ts
new file mode 100644
index 000000000..848a72da8
--- /dev/null
+++ b/tools-ng/tinymce/editor/src/core/main/ts/delete/MergeBlocks.ts
@@ -0,0 +1,109 @@
+/**
+ * MergeBlocks.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
+ */
+
+import { Arr, Option, Fun } from '@ephox/katamari';
+import { Compare, Insert, Remove, Element, Traverse } from '@ephox/sugar';
+import CaretFinder from '../caret/CaretFinder';
+import CaretPosition from '../caret/CaretPosition';
+import * as ElementType from '../dom/ElementType';
+import Empty from '../dom/Empty';
+import PaddingBr from '../dom/PaddingBr';
+import Parents from '../dom/Parents';
+
+const getChildrenUntilBlockBoundary = (block: Element) => {
+ const children = Traverse.children(block);
+ return Arr.findIndex(children, ElementType.isBlock).fold(
+ () => children,
+ (index) => children.slice(0, index)
+ );
+};
+
+const extractChildren = (block: Element) => {
+ const children = getChildrenUntilBlockBoundary(block);
+ Arr.each(children, Remove.remove);
+ return children;
+};
+
+const removeEmptyRoot = (rootNode: Element, block: Element) => {
+ const parents = Parents.parentsAndSelf(block, rootNode);
+ return Arr.find(parents.reverse(), Empty.isEmpty).each(Remove.remove);
+};
+
+const isEmptyBefore = (el: Element) => Arr.filter(Traverse.prevSiblings(el), (el) => !Empty.isEmpty(el)).length === 0;
+
+const nestedBlockMerge = (rootNode: Element, fromBlock: Element, toBlock: Element, insertionPoint: Element): Optiontext 1CHOPtext 2
+// would produce: +//text 1
CHOPtext 2
+// this function will then trim off empty edges and produce: +//text 1
CHOPtext 2
+const trimNode = function (dom, node) { + let i, children = node.childNodes; + + if (NodeType.isElement(node) && isBookmarkNode(node)) { + return; + } + + for (i = children.length - 1; i >= 0; i--) { + trimNode(dom, children[i]); + } + + if (NodeType.isDocument(node) === false) { + // Keep non whitespace text nodes + if (NodeType.isText(node) && node.nodeValue.length > 0) { + // Keep if parent element is a block or if there is some useful content + const trimmedLength = Tools.trim(node.nodeValue).length; + if (dom.isBlock(node.parentNode) || trimmedLength > 0) { + return; + } + // Also keep text nodes with only spaces if surrounded by spans. + // eg. "a b
" should keep space between a and b + if (trimmedLength === 0 && surroundedBySpans(node)) { + return; + } + } else if (NodeType.isElement(node)) { + // If the only child is a bookmark then move it up + children = node.childNodes; + + if (children.length === 1 && isBookmarkNode(children[0])) { + node.parentNode.insertBefore(children[0], node); + } + + // Keep non empty elements and void elements + if (children.length || ElementType.isVoid(Element.fromDom(node))) { + return; + } + } + + dom.remove(node); + } + return node; +}; + +export default { + trimNode +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/file/Conversions.ts b/tools-ng/tinymce/editor/src/core/main/ts/file/Conversions.ts new file mode 100644 index 000000000..5c1a9da7b --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/file/Conversions.ts @@ -0,0 +1,123 @@ +/** + * Conversions.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 + */ + +import { FileReader, Uint8Array, Window, XMLHttpRequest } from '@ephox/sand'; +import Promise from '../api/util/Promise'; +import { Blob } from '@ephox/dom-globals'; + +/** + * Converts blob/uris back and forth. + * + * @private + * @class tinymce.file.Conversions + */ + +const blobUriToBlob = function (url: string): Promise|
+ formatNode.parentNode.replaceChild(caretContainer, formatNode); + } else { + PaddingBr.removeTrailingBr(Element.fromDom(formatNode)); + if (dom.isEmpty(formatNode)) { + formatNode.parentNode.replaceChild(caretContainer, formatNode); + } else { + dom.insertAfter(caretContainer, formatNode); + } + } +}; + +const appendNode = function (parentNode, node) { + parentNode.appendChild(node); + return node; +}; + +const insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) { + const innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) { + return appendNode(parentNode, formatNode.cloneNode(false)); + }, caretContainer); + + return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP)); +}; + +const applyCaretFormat = function (editor, name, vars) { + let rng, caretContainer, textNode, offset, bookmark, container, text; + const selection = editor.selection; + + rng = selection.getRng(true); + offset = rng.startOffset; + container = rng.startContainer; + text = container.nodeValue; + + caretContainer = getParentCaretContainer(editor.getBody(), selection.getStart()); + if (caretContainer) { + textNode = findFirstTextNode(caretContainer); + } + + // Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character + const wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/; + if (text && offset > 0 && offset < text.length && + wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) { + // Get bookmark of caret position + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name)); + rng = SplitRange.split(rng); + + // Apply the format to the range + editor.formatter.apply(name, vars, rng); + + // Move selection back to caret position + selection.moveToBookmark(bookmark); + } else { + if (!caretContainer || textNode.nodeValue !== ZWSP) { + // Need to import the node into the document on IE or we get a lovely WrongDocument exception + caretContainer = importNode(editor.getDoc(), createCaretContainer(true).dom()); + textNode = caretContainer.firstChild; + + rng.insertNode(caretContainer); + offset = 1; + + editor.formatter.apply(name, vars, caretContainer); + } else { + editor.formatter.apply(name, vars, caretContainer); + } + + // Move selection to text node + selection.setCursorLocation(textNode, offset); + } +}; + +const removeCaretFormat = function (editor: Editor, name, vars, similar) { + const dom = editor.dom, selection: Selection = editor.selection; + let container, offset, bookmark; + let hasContentAfter, node, formatNode; + const parents = [], rng = selection.getRng(); + let caretContainer; + + container = rng.startContainer; + offset = rng.startOffset; + node = container; + + if (container.nodeType === 3) { + if (offset !== container.nodeValue.length) { + hasContentAfter = true; + } + + node = node.parentNode; + } + + while (node) { + if (MatchFormat.matchNode(editor, node, name, vars, similar)) { + formatNode = node; + break; + } + + if (node.nextSibling) { + hasContentAfter = true; + } + + parents.push(node); + node = node.parentNode; + } + + // Node doesn't have the specified format + if (!formatNode) { + return; + } + + // Is there contents after the caret then remove the format on the element + if (hasContentAfter) { + bookmark = selection.getBookmark(); + + // Collapse bookmark range (WebKit) + rng.collapse(true); + + // Expand the range to the closest word and split it at those points + let expandedRng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name), true); + expandedRng = SplitRange.split(expandedRng); + + editor.formatter.remove(name, vars, expandedRng); + selection.moveToBookmark(bookmark); + } else { + caretContainer = getParentCaretContainer(editor.getBody(), formatNode); + const newCaretContainer = createCaretContainer(false).dom(); + const caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer); + + if (caretContainer) { + insertCaretContainerNode(editor, newCaretContainer, caretContainer); + } else { + insertCaretContainerNode(editor, newCaretContainer, formatNode); + } + + removeCaretContainerNode(editor, caretContainer, false); + selection.setCursorLocation(caretNode, 1); + + if (dom.isEmpty(formatNode)) { + dom.remove(formatNode); + } + } +}; + +const disableCaretContainer = function (editor: Editor, keyCode: number) { + const selection = editor.selection, body = editor.getBody(); + + removeCaretContainer(editor, null, false); + + // Remove caret container if it's empty + if ((keyCode === 8 || keyCode === 46) && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) { + removeCaretContainer(editor, getParentCaretContainer(body, selection.getStart())); + } + + // Remove caret container on keydown and it's left/right arrow keys + if (keyCode === 37 || keyCode === 39) { + removeCaretContainer(editor, getParentCaretContainer(body, selection.getStart())); + } +}; + +const setup = function (editor: Editor) { + editor.on('mouseup keydown', function (e) { + disableCaretContainer(editor, e.keyCode); + }); +}; + +const replaceWithCaretFormat = function (targetNode, formatNodes) { + const caretContainer = createCaretContainer(false); + const innerMost = insertFormatNodesIntoCaretContainer(formatNodes, caretContainer.dom()); + Insert.before(Element.fromDom(targetNode), caretContainer); + Remove.remove(Element.fromDom(targetNode)); + + return CaretPosition(innerMost, 0); +}; + +const isFormatElement = function (editor: Editor, element: Element) { + const inlineElements = editor.schema.getTextInlineElements(); + return inlineElements.hasOwnProperty(SugarNode.name(element)) && !isCaretNode(element.dom()) && !NodeType.isBogus(element.dom()); +}; + +const isEmptyCaretFormatElement = function (element: Element) { + return isCaretNode(element.dom()) && isCaretContainerEmpty(element.dom()); +}; + +export { + setup, + applyCaretFormat, + removeCaretFormat, + replaceWithCaretFormat, + isFormatElement, + isEmptyCaretFormatElement +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/fmt/DefaultFormats.ts b/tools-ng/tinymce/editor/src/core/main/ts/fmt/DefaultFormats.ts new file mode 100644 index 000000000..c33f252eb --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/fmt/DefaultFormats.ts @@ -0,0 +1,199 @@ +/** + * DefaultFormats.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 + */ + +import Tools from '../api/util/Tools'; + +const get = function (dom) { + const formats = { + valigntop: [ + { selector: 'td,th', styles: { verticalAlign: 'top' } } + ], + + valignmiddle: [ + { selector: 'td,th', styles: { verticalAlign: 'middle' } } + ], + + valignbottom: [ + { selector: 'td,th', styles: { verticalAlign: 'bottom' } } + ], + + alignleft: [ + { + selector: 'figure.image', + collapsed: false, + classes: 'align-left', + ceFalseOverride: true, + preview: 'font-family font-size' + }, + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'left' + }, + inherit: false, + preview: false, + defaultBlock: 'div' + }, + { + selector: 'img,table', + collapsed: false, + styles: { + float: 'left' + }, + preview: 'font-family font-size' + } + ], + + aligncenter: [ + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'center' + }, + inherit: false, + preview: 'font-family font-size', + defaultBlock: 'div' + }, + { + selector: 'figure.image', + collapsed: false, + classes: 'align-center', + ceFalseOverride: true, + preview: 'font-family font-size' + }, + { + selector: 'img', + collapsed: false, + styles: { + display: 'block', + marginLeft: 'auto', + marginRight: 'auto' + }, + preview: false + }, + { + selector: 'table', + collapsed: false, + styles: { + marginLeft: 'auto', + marginRight: 'auto' + }, + preview: 'font-family font-size' + } + ], + + alignright: [ + { + selector: 'figure.image', + collapsed: false, + classes: 'align-right', + ceFalseOverride: true, + preview: 'font-family font-size' + }, + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'right' + }, + inherit: false, + preview: 'font-family font-size', + defaultBlock: 'div' + }, + { + selector: 'img,table', + collapsed: false, + styles: { + float: 'right' + }, + preview: 'font-family font-size' + } + ], + + alignjustify: [ + { + selector: 'figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li', + styles: { + textAlign: 'justify' + }, + inherit: false, + defaultBlock: 'div', + preview: 'font-family font-size' + } + ], + + bold: [ + { inline: 'strong', remove: 'all' }, + { inline: 'span', styles: { fontWeight: 'bold' } }, + { inline: 'b', remove: 'all' } + ], + + italic: [ + { inline: 'em', remove: 'all' }, + { inline: 'span', styles: { fontStyle: 'italic' } }, + { inline: 'i', remove: 'all' } + ], + + underline: [ + { inline: 'span', styles: { textDecoration: 'underline' }, exact: true }, + { inline: 'u', remove: 'all' } + ], + + strikethrough: [ + { inline: 'span', styles: { textDecoration: 'line-through' }, exact: true }, + { inline: 'strike', remove: 'all' } + ], + + forecolor: { inline: 'span', styles: { color: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, + hilitecolor: { inline: 'span', styles: { backgroundColor: '%value' }, links: true, remove_similar: true, clear_child_styles: true }, + fontname: { inline: 'span', toggle: false, styles: { fontFamily: '%value' }, clear_child_styles: true }, + fontsize: { inline: 'span', toggle: false, styles: { fontSize: '%value' }, clear_child_styles: true }, + fontsize_class: { inline: 'span', attributes: { class: '%value' } }, + blockquote: { block: 'blockquote', wrapper: 1, remove: 'all' }, + subscript: { inline: 'sub' }, + superscript: { inline: 'sup' }, + code: { inline: 'code' }, + + link: { + inline: 'a', selector: 'a', remove: 'all', split: true, deep: true, + onmatch () { + return true; + }, + + onformat (elm, fmt, vars) { + Tools.each(vars, function (value, key) { + dom.setAttrib(elm, key, value); + }); + } + }, + + removeformat: [ + { + selector: 'b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins', + remove: 'all', + split: true, + expand: false, + block_expand: true, + deep: true + }, + { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true }, + { selector: '*', attributes: ['style', 'class'], split: false, expand: false, deep: true } + ] + }; + + Tools.each('p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp'.split(/\s/), function (name) { + formats[name] = { block: name, remove: 'all' }; + }); + + return formats; +}; + +export default { + get +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/fmt/ExpandRange.ts b/tools-ng/tinymce/editor/src/core/main/ts/fmt/ExpandRange.ts new file mode 100644 index 000000000..3ea1f3a19 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/fmt/ExpandRange.ts @@ -0,0 +1,391 @@ +/** + * ExpandRange.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 + */ + +import Bookmarks from '../bookmark/Bookmarks'; +import TreeWalker from '../api/dom/TreeWalker'; +import FormatUtils from './FormatUtils'; +import * as RangeNodes from '../selection/RangeNodes'; + +const isBookmarkNode = Bookmarks.isBookmarkNode; +const getParents = FormatUtils.getParents, isWhiteSpaceNode = FormatUtils.isWhiteSpaceNode, isTextBlock = FormatUtils.isTextBlock; + +// This function walks down the tree to find the leaf at the selection. +// The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. +const findLeaf = function (node, offset?) { + if (typeof offset === 'undefined') { + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + + while (node && node.hasChildNodes()) { + node = node.childNodes[offset]; + if (node) { + offset = node.nodeType === 3 ? node.length : node.childNodes.length; + } + } + + return { node, offset }; +}; + +const excludeTrailingWhitespace = function (endContainer, endOffset) { + // Avoid applying formatting to a trailing space, + // but remove formatting from trailing space + let leaf = findLeaf(endContainer, endOffset); + if (leaf.node) { + while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { + leaf = findLeaf(leaf.node.previousSibling); + } + + if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && + leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { + + if (leaf.offset > 1) { + endContainer = leaf.node; + endContainer.splitText(leaf.offset - 1); + } + } + } + + return endContainer; +}; + +const isBogusBr = function (node) { + return node.nodeName === 'BR' && node.getAttribute('data-mce-bogus') && !node.nextSibling; +}; + +// Expands the node to the closes contentEditable false element if it exists +const findParentContentEditable = function (dom, node) { + let parent = node; + + while (parent) { + if (parent.nodeType === 1 && dom.getContentEditable(parent)) { + return dom.getContentEditable(parent) === 'false' ? parent : node; + } + + parent = parent.parentNode; + } + + return node; +}; + +const findSpace = function (start, remove, node, offset?) { + let pos, pos2; + const str = node.nodeValue; + + if (typeof offset === 'undefined') { + offset = start ? str.length : 0; + } + + if (start) { + pos = str.lastIndexOf(' ', offset); + pos2 = str.lastIndexOf('\u00a0', offset); + pos = pos > pos2 ? pos : pos2; + + // Include the space on remove to avoid tag soup + // As long as we are either going to the right, + // - OR - going to the left and pos isn't already at the end of the string + if (pos !== -1 && !remove && (pos < offset || !start) && pos <= str.length) { + pos++; + } + } else { + pos = str.indexOf(' ', offset); + pos2 = str.indexOf('\u00a0', offset); + pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; + } + + return pos; +}; + +const findWordEndPoint = function (dom, body, container, offset, start, remove) { + let walker, node, pos, lastTextNode; + + if (container.nodeType === 3) { + pos = findSpace(start, remove, container, offset); + + if (pos !== -1) { + return { container, offset: pos }; + } + + lastTextNode = container; + } + + // Walk the nodes inside the block + walker = new TreeWalker(container, dom.getParent(container, dom.isBlock) || body); + while ((node = walker[start ? 'prev' : 'next']())) { + if (node.nodeType === 3 && !isBookmarkNode(node.parentNode)) { + lastTextNode = node; + pos = findSpace(start, remove, node); + + if (pos !== -1) { + return { container: node, offset: pos }; + } + } else if (dom.isBlock(node) || FormatUtils.isEq(node, 'BR')) { + break; + } + } + + if (lastTextNode) { + if (start) { + offset = 0; + } else { + offset = lastTextNode.length; + } + + return { container: lastTextNode, offset }; + } +}; + +const findSelectorEndPoint = function (dom, format, rng, container, siblingName) { + let parents, i, y, curFormat; + + if (container.nodeType === 3 && container.nodeValue.length === 0 && container[siblingName]) { + container = container[siblingName]; + } + + parents = getParents(dom, container); + for (i = 0; i < parents.length; i++) { + for (y = 0; y < format.length; y++) { + curFormat = format[y]; + + // If collapsed state is set then skip formats that doesn't match that + if ('collapsed' in curFormat && curFormat.collapsed !== rng.collapsed) { + continue; + } + + if (dom.is(parents[i], curFormat.selector)) { + return parents[i]; + } + } + } + + return container; +}; + +const findBlockEndPoint = function (editor, format, container, siblingName) { + let node; + const dom = editor.dom; + const root = dom.getRoot(); + + // Expand to block of similar type + if (!format[0].wrapper) { + node = dom.getParent(container, format[0].block, root); + } + + // Expand to first wrappable block element or any block element + if (!node) { + const scopeRoot = dom.getParent(container, 'LI,TD,TH'); + node = dom.getParent(container.nodeType === 3 ? container.parentNode : container, function (node) { + // Fixes #6183 where it would expand to editable parent element in inline mode + return node !== root && isTextBlock(editor, node); + }, scopeRoot); + } + + // Exclude inner lists from wrapping + if (node && format[0].wrapper) { + node = getParents(dom, node, 'ul,ol').reverse()[0] || node; + } + + // Didn't find a block element look for first/last wrappable element + if (!node) { + node = container; + + while (node[siblingName] && !dom.isBlock(node[siblingName])) { + node = node[siblingName]; + + // Break on BR but include it will be removed later on + // we can't remove it now since we need to check if it can be wrapped + if (FormatUtils.isEq(node, 'br')) { + break; + } + } + } + + return node || container; +}; + +// This function walks up the tree if there is no siblings before/after the node +const findParentContainer = function (dom, format, startContainer, startOffset, endContainer, endOffset, start) { + let container, parent, sibling, siblingName, root; + + container = parent = start ? startContainer : endContainer; + siblingName = start ? 'previousSibling' : 'nextSibling'; + root = dom.getRoot(); + + // If it's a text node and the offset is inside the text + if (container.nodeType === 3 && !isWhiteSpaceNode(container)) { + if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { + return container; + } + } + + /*eslint no-constant-condition:0 */ + while (true) { + // Stop expanding on block elements + if (!format[0].block_expand && dom.isBlock(parent)) { + return parent; + } + + // Walk left/right + for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { + if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { + return parent; + } + } + + // Check if we can move up are we at root level or body level + if (parent === root || parent.parentNode === root) { + container = parent; + break; + } + + parent = parent.parentNode; + } + + return container; +}; + +const expandRng = function (editor, rng, format, remove?) { + let endPoint, + startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset; + const dom = editor.dom; + + // If index based start position then resolve it + if (startContainer.nodeType === 1 && startContainer.hasChildNodes()) { + startContainer = RangeNodes.getNode(startContainer, startOffset); + if (startContainer.nodeType === 3) { + startOffset = 0; + } + } + + // If index based end position then resolve it + if (endContainer.nodeType === 1 && endContainer.hasChildNodes()) { + endContainer = RangeNodes.getNode(endContainer, rng.collapsed ? endOffset : endOffset - 1); + if (endContainer.nodeType === 3) { + endOffset = endContainer.nodeValue.length; + } + } + + // Expand to closest contentEditable element + startContainer = findParentContentEditable(dom, startContainer); + endContainer = findParentContentEditable(dom, endContainer); + + // Exclude bookmark nodes if possible + if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { + startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; + if (rng.collapsed) { + startContainer = startContainer.previousSibling || startContainer; + } else { + startContainer = startContainer.nextSibling || startContainer; + } + + if (startContainer.nodeType === 3) { + startOffset = rng.collapsed ? startContainer.length : 0; + } + } + + if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { + endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; + if (rng.collapsed) { + endContainer = endContainer.nextSibling || endContainer; + } else { + endContainer = endContainer.previousSibling || endContainer; + } + + if (endContainer.nodeType === 3) { + endOffset = rng.collapsed ? 0 : endContainer.length; + } + } + + if (rng.collapsed) { + // Expand left to closest word boundary + endPoint = findWordEndPoint(dom, editor.getBody(), startContainer, startOffset, true, remove); + if (endPoint) { + startContainer = endPoint.container; + startOffset = endPoint.offset; + } + + // Expand right to closest word boundary + endPoint = findWordEndPoint(dom, editor.getBody(), endContainer, endOffset, false, remove); + if (endPoint) { + endContainer = endPoint.container; + endOffset = endPoint.offset; + } + } + + if (format[0].inline) { + // For "removeformat", we include trailing whitespace. For other formatting, we don't + endContainer = remove ? endContainer : excludeTrailingWhitespace(endContainer, endOffset); + } + + // Move start/end point up the tree if the leaves are sharp and if we are in different containers + // Example * becomes !: !*texttext*
! + // This will reduce the number of wrapper elements that needs to be created + // Move start point up the tree + if (format[0].inline || format[0].block_expand) { + if (!format[0].inline || (startContainer.nodeType !== 3 || startOffset === 0)) { + startContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, true); + } + + if (!format[0].inline || (endContainer.nodeType !== 3 || endOffset === endContainer.nodeValue.length)) { + endContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, false); + } + } + + // Expand start/end container to matching selector + if (format[0].selector && format[0].expand !== false && !format[0].inline) { + // Find new startContainer/endContainer if there is better one + startContainer = findSelectorEndPoint(dom, format, rng, startContainer, 'previousSibling'); + endContainer = findSelectorEndPoint(dom, format, rng, endContainer, 'nextSibling'); + } + + // Expand start/end container to matching block element or text node + if (format[0].block || format[0].selector) { + // Find new startContainer/endContainer if there is better one + startContainer = findBlockEndPoint(editor, format, startContainer, 'previousSibling'); + endContainer = findBlockEndPoint(editor, format, endContainer, 'nextSibling'); + + // Non block element then try to expand up the leaf + if (format[0].block) { + if (!dom.isBlock(startContainer)) { + startContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, true); + } + + if (!dom.isBlock(endContainer)) { + endContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, false); + } + } + } + + // Setup index for startContainer + if (startContainer.nodeType === 1) { + startOffset = dom.nodeIndex(startContainer); + startContainer = startContainer.parentNode; + } + + // Setup index for endContainer + if (endContainer.nodeType === 1) { + endOffset = dom.nodeIndex(endContainer) + 1; + endContainer = endContainer.parentNode; + } + + // Return new range like object + return { + startContainer, + startOffset, + endContainer, + endOffset + }; +}; + +export default { + expandRng +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/fmt/FontInfo.ts b/tools-ng/tinymce/editor/src/core/main/ts/fmt/FontInfo.ts new file mode 100644 index 000000000..3b139a513 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/fmt/FontInfo.ts @@ -0,0 +1,62 @@ +/** + * FontInfo.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 + */ + +import { Fun, Option } from '@ephox/katamari'; +import { Element as SugarElement, Node as SugarNode, PredicateFind, Css, Compare } from '@ephox/sugar'; +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import { Element, HTMLElement, Node } from '@ephox/dom-globals'; + +const getSpecifiedFontProp = (propName: string, rootElm: Element, elm: HTMLElement): Optiona
+ lastParent = node; + while (parent && parent.firstChild === lastParent && parent.lastChild === lastParent) { + lastParent = parent; + + if (blockElements[parent.name]) { + break; + } + + parent = parent.parent; + } + + if (lastParent === parent && settings.padd_empty_with_br !== true) { + textNode = new Node('#text', 3); + textNode.value = '\u00a0'; + node.replace(textNode); + } + } + } + }); + } + + parser.addAttributeFilter('href', function (nodes) { + let i = nodes.length, node; + + const appendRel = function (rel) { + const parts = rel.split(' ').filter(function (p) { + return p.length > 0; + }); + return parts.concat(['noopener']).sort().join(' '); + }; + + const addNoOpener = function (rel) { + const newRel = rel ? Tools.trim(rel) : ''; + if (!/\b(noopener)\b/g.test(newRel)) { + return appendRel(newRel); + } else { + return newRel; + } + }; + + if (!settings.allow_unsafe_link_target) { + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.attr('target') === '_blank') { + node.attr('rel', addNoOpener(node.attr('rel'))); + } + } + } + }); + + // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. + if (!settings.allow_html_in_named_anchor) { + parser.addAttributeFilter('id,name', function (nodes) { + let i = nodes.length, sibling, prevSibling, parent, node; + + while (i--) { + node = nodes[i]; + if (node.name === 'a' && node.firstChild && !node.attr('href')) { + parent = node.parent; + + // Move children after current node + sibling = node.lastChild; + do { + prevSibling = sibling.prev; + parent.insert(sibling, node); + sibling = prevSibling; + } while (sibling); + } + } + }); + } + + if (settings.fix_list_elements) { + parser.addNodeFilter('ul,ol', function (nodes) { + let i = nodes.length, node, parentNode; + + while (i--) { + node = nodes[i]; + parentNode = node.parent; + + if (parentNode.name === 'ul' || parentNode.name === 'ol') { + if (node.prev && node.prev.name === 'li') { + node.prev.append(node); + } else { + const li = new Node('li', 1); + li.attr('style', 'list-style-type: none'); + node.wrap(li); + } + } + } + }); + } + + if (settings.validate && schema.getValidClasses()) { + parser.addAttributeFilter('class', function (nodes) { + let i = nodes.length, node, classList, ci, className, classValue; + const validClasses = schema.getValidClasses(); + let validClassesMap, valid; + + while (i--) { + node = nodes[i]; + classList = node.attr('class').split(' '); + classValue = ''; + + for (ci = 0; ci < classList.length; ci++) { + className = classList[ci]; + valid = false; + + validClassesMap = validClasses['*']; + if (validClassesMap && validClassesMap[className]) { + valid = true; + } + + validClassesMap = validClasses[node.name]; + if (!valid && validClassesMap && validClassesMap[className]) { + valid = true; + } + + if (valid) { + if (classValue) { + classValue += ' '; + } + + classValue += className; + } + } + + if (!classValue.length) { + classValue = null; + } + + node.attr('class', classValue); + } + }); + } +}; + +export { + register +}; diff --git a/tools-ng/tinymce/editor/src/core/main/ts/html/ParserUtils.ts b/tools-ng/tinymce/editor/src/core/main/ts/html/ParserUtils.ts new file mode 100644 index 000000000..82f3d0aa0 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/html/ParserUtils.ts @@ -0,0 +1,40 @@ +import Node from '../api/html/Node'; + +const paddEmptyNode = function (settings, args, blockElements, node) { + const brPreferred = settings.padd_empty_with_br || args.insert; + + if (brPreferred && blockElements[node.name]) { + node.empty().append(new Node('br', 1)).shortEnded = true; + } else { + node.empty().append(new Node('#text', 3)).value = '\u00a0'; + } +}; + +const isPaddedWithNbsp = function (node) { + return hasOnlyChild(node, '#text') && node.firstChild.value === '\u00a0'; +}; + +const hasOnlyChild = function (node, name) { + return node && node.firstChild && node.firstChild === node.lastChild && node.firstChild.name === name; +}; + +const isPadded = function (schema, node) { + const rule = schema.getElementRule(node.name); + return rule && rule.paddEmpty; +}; + +const isEmpty = function (schema, nonEmptyElements, whitespaceElements, node) { + return node.isEmpty(nonEmptyElements, whitespaceElements, function (node) { + return isPadded(schema, node); + }); +}; + +const isLineBreakNode = (node, blockElements) => node && (blockElements[node.name] || node.name === 'br'); + +export { + paddEmptyNode, + isPaddedWithNbsp, + hasOnlyChild, + isEmpty, + isLineBreakNode +}; diff --git a/tools-ng/tinymce/editor/src/core/main/ts/init/Init.ts b/tools-ng/tinymce/editor/src/core/main/ts/init/Init.ts new file mode 100644 index 000000000..f309c0c85 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/init/Init.ts @@ -0,0 +1,205 @@ +/** + * Init.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 + */ + +import { Type } from '@ephox/katamari'; +import PluginManager from '../api/PluginManager'; +import ThemeManager from '../api/ThemeManager'; +import DOMUtils from '../api/dom/DOMUtils'; +import InitContentBody from './InitContentBody'; +import InitIframe from './InitIframe'; +import Tools from '../api/util/Tools'; + +const DOM = DOMUtils.DOM; + +const initPlugin = function (editor, initializedPlugins, plugin) { + const Plugin = PluginManager.get(plugin); + let pluginUrl, pluginInstance; + + pluginUrl = PluginManager.urls[plugin] || editor.documentBaseUrl.replace(/\/$/, ''); + plugin = Tools.trim(plugin); + if (Plugin && Tools.inArray(initializedPlugins, plugin) === -1) { + Tools.each(PluginManager.dependencies(plugin), function (dep) { + initPlugin(editor, initializedPlugins, dep); + }); + + if (editor.plugins[plugin]) { + return; + } + + pluginInstance = new Plugin(editor, pluginUrl, editor.$); + + editor.plugins[plugin] = pluginInstance; + + if (pluginInstance.init) { + pluginInstance.init(editor, pluginUrl); + initializedPlugins.push(plugin); + } + } +}; + +const trimLegacyPrefix = function (name) { + // Themes and plugins can be prefixed with - to prevent them from being lazy loaded + return name.replace(/^\-/, ''); +}; + +const initPlugins = function (editor) { + const initializedPlugins = []; + + Tools.each(editor.settings.plugins.split(/[ ,]/), function (name) { + initPlugin(editor, initializedPlugins, trimLegacyPrefix(name)); + }); +}; + +const initTheme = function (editor) { + let Theme; + const theme = editor.settings.theme; + + if (Type.isString(theme)) { + editor.settings.theme = trimLegacyPrefix(theme); + + Theme = ThemeManager.get(theme); + editor.theme = new Theme(editor, ThemeManager.urls[theme]); + + if (editor.theme.init) { + editor.theme.init(editor, ThemeManager.urls[theme] || editor.documentBaseUrl.replace(/\/$/, ''), editor.$); + } + } else { + // Theme set to false or null doesn't produce a theme api + editor.theme = {}; + } +}; + +const renderFromLoadedTheme = function (editor) { + let w, h, minHeight, re, info; + const settings = editor.settings; + const elm = editor.getElement(); + + w = settings.width || DOM.getStyle(elm, 'width') || '100%'; + h = settings.height || DOM.getStyle(elm, 'height') || elm.offsetHeight; + minHeight = settings.min_height || 100; + re = /^[0-9\.]+(|px)$/i; + + if (re.test('' + w)) { + w = Math.max(parseInt(w, 10), 100); + } + + if (re.test('' + h)) { + h = Math.max(parseInt(h, 10), minHeight); + } + + // Render UI + info = editor.theme.renderUI({ + targetNode: elm, + width: w, + height: h, + deltaWidth: settings.delta_width, + deltaHeight: settings.delta_height + }); + + // Resize editor + if (!settings.content_editable) { + h = (info.iframeHeight || h) + (typeof h === 'number' ? (info.deltaHeight || 0) : ''); + if (h < minHeight) { + h = minHeight; + } + } + + info.height = h; + + return info; +}; + +const renderFromThemeFunc = function (editor) { + let info; + const elm = editor.getElement(); + + info = editor.settings.theme(editor, elm); + + if (info.editorContainer.nodeType) { + info.editorContainer.id = info.editorContainer.id || editor.id + '_parent'; + } + + if (info.iframeContainer && info.iframeContainer.nodeType) { + info.iframeContainer.id = info.iframeContainer.id || editor.id + '_iframecontainer'; + } + + info.height = info.iframeHeight ? info.iframeHeight : elm.offsetHeight; + + return info; +}; + +const createThemeFalseResult = function (element) { + return { + editorContainer: element, + iframeContainer: element + }; +}; + +const renderThemeFalseIframe = function (targetElement) { + const iframeContainer = DOM.create('div'); + + DOM.insertAfter(iframeContainer, targetElement); + + return createThemeFalseResult(iframeContainer); +}; + +const renderThemeFalse = function (editor) { + const targetElement = editor.getElement(); + return editor.inline ? createThemeFalseResult(null) : renderThemeFalseIframe(targetElement); +}; + +const renderThemeUi = function (editor) { + const settings = editor.settings, elm = editor.getElement(); + + editor.orgDisplay = elm.style.display; + + if (Type.isString(settings.theme)) { + return renderFromLoadedTheme(editor); + } else if (Type.isFunction(settings.theme)) { + return renderFromThemeFunc(editor); + } else { + return renderThemeFalse(editor); + } +}; + +const init = function (editor) { + const settings = editor.settings; + const elm = editor.getElement(); + let boxInfo; + + editor.rtl = settings.rtl_ui || editor.editorManager.i18n.rtl; + editor.editorManager.i18n.setCode(settings.language); + settings.aria_label = settings.aria_label || DOM.getAttrib(elm, 'aria-label', editor.getLang('aria.rich_text_area')); + + editor.fire('ScriptsLoaded'); + + initTheme(editor); + initPlugins(editor); + boxInfo = renderThemeUi(editor); + editor.editorContainer = boxInfo.editorContainer ? boxInfo.editorContainer : null; + + // Load specified content CSS last + if (settings.content_css) { + Tools.each(Tools.explode(settings.content_css), function (u) { + editor.contentCSS.push(editor.documentBaseURI.toAbsolute(u)); + }); + } + + // Content editable mode ends here + if (settings.content_editable) { + return InitContentBody.initContentBody(editor); + } else { + return InitIframe.init(editor, boxInfo); + } +}; + +export default { + init +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/init/InitContentBody.ts b/tools-ng/tinymce/editor/src/core/main/ts/init/InitContentBody.ts new file mode 100644 index 000000000..359066661 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/init/InitContentBody.ts @@ -0,0 +1,312 @@ +/** + * InitContentBody.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 + */ + +import { Insert, Element, Attr } from '@ephox/sugar'; +import EditorUpload from '../api/EditorUpload'; +import ForceBlocks from '../ForceBlocks'; +import NodeChange from '../NodeChange'; +import SelectionOverrides from '../SelectionOverrides'; +import UndoManager from '../api/UndoManager'; +import Annotator from '../api/Annotator'; +import Formatter from '../api/Formatter'; +import Serializer from '../api/dom/Serializer'; +import DOMUtils from '../api/dom/DOMUtils'; +import { Selection } from '../api/dom/Selection'; +import DomParser from '../api/html/DomParser'; +import Node from '../api/html/Node'; +import Schema from '../api/html/Schema'; +import KeyboardOverrides from '../keyboard/KeyboardOverrides'; +import Delay from '../api/util/Delay'; +import Quirks from '../util/Quirks'; +import Tools from '../api/util/Tools'; +import { Editor } from 'tinymce/core/api/Editor'; +import * as MultiClickSelection from 'tinymce/core/selection/MultiClickSelection'; +import * as DetailsElement from '../selection/DetailsElement'; +import { document, window } from '@ephox/dom-globals'; +import Settings from '../api/Settings'; + +declare const escape: any; + +const DOM = DOMUtils.DOM; + +const appendStyle = function (editor: Editor, text: string) { + const head = Element.fromDom(editor.getDoc().head); + const tag = Element.fromTag('style'); + Attr.set(tag, 'type', 'text/css'); + Insert.append(tag, Element.fromText(text)); + Insert.append(head, tag); +}; + +const createParser = function (editor: Editor) { + const parser = DomParser(editor.settings, editor.schema); + + // Convert src and href into data-mce-src, data-mce-href and data-mce-style + parser.addAttributeFilter('src,href,style,tabindex', function (nodes, name) { + let i = nodes.length, node; + const dom = editor.dom; + let value, internalName; + + while (i--) { + node = nodes[i]; + value = node.attr(name); + internalName = 'data-mce-' + name; + + // Add internal attribute if we need to we don't on a refresh of the document + if (!node.attributes.map[internalName]) { + // Don't duplicate these since they won't get modified by any browser + if (value.indexOf('data:') === 0 || value.indexOf('blob:') === 0) { + continue; + } + + if (name === 'style') { + value = dom.serializeStyle(dom.parseStyle(value), node.name); + + if (!value.length) { + value = null; + } + + node.attr(internalName, value); + node.attr(name, value); + } else if (name === 'tabindex') { + node.attr(internalName, value); + node.attr(name, null); + } else { + node.attr(internalName, editor.convertURL(value, name, node.name)); + } + } + } + }); + + // Keep scripts from executing + parser.addNodeFilter('script', function (nodes: Node[]) { + let i = nodes.length, node, type; + + while (i--) { + node = nodes[i]; + type = node.attr('type') || 'no/type'; + if (type.indexOf('mce-') !== 0) { + node.attr('type', 'mce-' + type); + } + } + }); + + parser.addNodeFilter('#cdata', function (nodes: Node[]) { + let i = nodes.length, node; + + while (i--) { + node = nodes[i]; + node.type = 8; + node.name = '#comment'; + node.value = '[CDATA[' + node.value + ']]'; + } + }); + + parser.addNodeFilter('p,h1,h2,h3,h4,h5,h6,div', function (nodes: Node[]) { + let i = nodes.length, node; + const nonEmptyElements = editor.schema.getNonEmptyElements(); + + while (i--) { + node = nodes[i]; + + if (node.isEmpty(nonEmptyElements) && node.getAll('br').length === 0) { + node.append(new Node('br', 1)).shortEnded = true; + } + } + }); + + return parser; +}; + +const autoFocus = function (editor: Editor) { + if (editor.settings.auto_focus) { + Delay.setEditorTimeout(editor, function () { + let focusEditor; + + if (editor.settings.auto_focus === true) { + focusEditor = editor; + } else { + focusEditor = editor.editorManager.get(editor.settings.auto_focus); + } + + if (!focusEditor.destroyed) { + focusEditor.focus(); + } + }, 100); + } +}; + +const initEditor = function (editor: Editor) { + editor.bindPendingEventDelegates(); + editor.initialized = true; + editor.fire('init'); + editor.focus(true); + editor.nodeChanged({ initial: true }); + editor.execCallback('init_instance_callback', editor); + autoFocus(editor); +}; + +const getStyleSheetLoader = function (editor: Editor) { + return editor.inline ? DOM.styleSheetLoader : editor.dom.styleSheetLoader; +}; + +const initContentBody = function (editor: Editor, skipWrite?: boolean) { + const settings = editor.settings; + const targetElm = editor.getElement(); + let doc = editor.getDoc(), body, contentCssText; + + // Restore visibility on target element + if (!settings.inline) { + editor.getElement().style.visibility = editor.orgVisibility; + } + + // Setup iframe body + if (!skipWrite && !settings.content_editable) { + doc.open(); + doc.write(editor.iframeHTML); + doc.close(); + } + + if (settings.content_editable) { + editor.on('remove', function () { + const bodyEl = this.getBody(); + + DOM.removeClass(bodyEl, 'mce-content-body'); + DOM.removeClass(bodyEl, 'mce-edit-focus'); + DOM.setAttrib(bodyEl, 'contentEditable', null); + }); + + DOM.addClass(targetElm, 'mce-content-body'); + editor.contentDocument = doc = settings.content_document || document; + editor.contentWindow = settings.content_window || window; + editor.bodyElement = targetElm; + + // Prevent leak in IE + settings.content_document = settings.content_window = null; + + // TODO: Fix this + settings.root_name = targetElm.nodeName.toLowerCase(); + } + + // It will not steal focus while setting contentEditable + body = editor.getBody(); + body.disabled = true; + editor.readonly = settings.readonly; + + if (!editor.readonly) { + if (editor.inline && DOM.getStyle(body, 'position', true) === 'static') { + body.style.position = 'relative'; + } + + body.contentEditable = editor.getParam('content_editable_state', true); + } + + body.disabled = false; + + editor.editorUpload = EditorUpload(editor); + editor.schema = Schema(settings); + editor.dom = DOMUtils(doc, { + keep_values: true, + url_converter: editor.convertURL, + url_converter_scope: editor, + hex_colors: settings.force_hex_style_colors, + class_filter: settings.class_filter, + update_styles: true, + root_element: editor.inline ? editor.getBody() : null, + collect: settings.content_editable, + schema: editor.schema, + contentCssCors: Settings.shouldUseContentCssCors(editor), + onSetAttrib (e) { + editor.fire('SetAttrib', e); + } + }); + + editor.parser = createParser(editor); + editor.serializer = Serializer(settings, editor); + editor.selection = Selection(editor.dom, editor.getWin(), editor.serializer, editor); + editor.annotator = Annotator(editor); + editor.formatter = Formatter(editor); + editor.undoManager = UndoManager(editor); + editor._nodeChangeDispatcher = new NodeChange(editor); + editor._selectionOverrides = SelectionOverrides(editor); + + DetailsElement.setup(editor); + MultiClickSelection.setup(editor); + KeyboardOverrides.setup(editor); + ForceBlocks.setup(editor); + + editor.fire('PreInit'); + + if (!settings.browser_spellcheck && !settings.gecko_spellcheck) { + doc.body.spellcheck = false; // Gecko + DOM.setAttrib(body, 'spellcheck', 'false'); + } + + editor.quirks = Quirks(editor); + editor.fire('PostRender'); + + if (settings.directionality) { + body.dir = settings.directionality; + } + + if (settings.nowrap) { + body.style.whiteSpace = 'nowrap'; + } + + if (settings.protect) { + editor.on('BeforeSetContent', function (e) { + Tools.each(settings.protect, function (pattern) { + e.content = e.content.replace(pattern, function (str) { + return ''; + }); + }); + }); + } + + editor.on('SetContent', function () { + editor.addVisual(editor.getBody()); + }); + + editor.load({ initial: true, format: 'html' }); + editor.startContent = editor.getContent({ format: 'raw' }) as string; + + editor.on('compositionstart compositionend', function (e) { + editor.composing = e.type === 'compositionstart'; + }); + + // Add editor specific CSS styles + if (editor.contentStyles.length > 0) { + contentCssText = ''; + + Tools.each(editor.contentStyles, function (style) { + contentCssText += style + '\r\n'; + }); + + editor.dom.addStyle(contentCssText); + } + + getStyleSheetLoader(editor).loadAll( + editor.contentCSS, + function (_) { + initEditor(editor); + }, + function (urls) { + initEditor(editor); + } + ); + + // Append specified content CSS last + if (settings.content_style) { + appendStyle(editor, settings.content_style); + } +}; + +export default { + initContentBody +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/init/InitIframe.ts b/tools-ng/tinymce/editor/src/core/main/ts/init/InitIframe.ts new file mode 100644 index 000000000..09d134796 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/init/InitIframe.ts @@ -0,0 +1,139 @@ +/** + * InitIframe.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 + */ + +import { Element, Attr, Css } from '@ephox/sugar'; +import Env from '../api/Env'; +import Settings from '../api/Settings'; +import DOMUtils from '../api/dom/DOMUtils'; +import InitContentBody from './InitContentBody'; +import Uuid from '../util/Uuid'; +import { document, window } from '@ephox/dom-globals'; + +const DOM = DOMUtils.DOM; + +const relaxDomain = function (editor, ifr) { + // Domain relaxing is required since the user has messed around with document.domain + // This only applies to IE 11 other browsers including Edge seems to handle document.domain + if (document.domain !== window.location.hostname && Env.ie && Env.ie < 12) { + const bodyUuid = Uuid.uuid('mce'); + + editor[bodyUuid] = function () { + InitContentBody.initContentBody(editor); + }; + + /*eslint no-script-url:0 */ + const domainRelaxUrl = 'javascript:(function(){' + + 'document.open();document.domain="' + document.domain + '";' + + 'var ed = window.parent.tinymce.get("' + editor.id + '");document.write(ed.iframeHTML);' + + 'document.close();ed.' + bodyUuid + '(true);})()'; + + DOM.setAttrib(ifr, 'src', domainRelaxUrl); + return true; + } + + return false; +}; + +const normalizeHeight = function (height) { + const normalizedHeight = typeof height === 'number' ? height + 'px' : height; + return normalizedHeight ? normalizedHeight : ''; +}; + +const createIframeElement = function (id, title, height, customAttrs) { + const iframe = Element.fromTag('iframe'); + + Attr.setAll(iframe, customAttrs); + + Attr.setAll(iframe, { + id: id + '_ifr', + frameBorder: '0', + allowTransparency: 'true', + title + }); + + Css.setAll(iframe, { + width: '100%', + height: normalizeHeight(height), + display: 'block' // Important for Gecko to render the iframe correctly + }); + + return iframe; +}; + +const getIframeHtml = function (editor) { + let bodyId, bodyClass, iframeHTML; + + iframeHTML = Settings.getDocType(editor) + ''; + + // We only need to override paths if we have to + // IE has a bug where it remove site absolute urls to relative ones if this is specified + if (Settings.getDocumentBaseUrl(editor) !== editor.documentBaseUrl) { + iframeHTML += '
abc|
x
becomes this:x
+const trimInlineElementsOnLeftSideOfBlock = function (dom, nonEmptyElementsMap, block) { + let node = block; + const firstChilds = []; + let i; + + if (!node) { + return; + } + + // Find inner most first child ex:*
+ while ((node = node.firstChild)) { + if (dom.isBlock(node)) { + return; + } + + if (NodeType.isElement(node) && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + firstChilds.push(node); + } + } + + i = firstChilds.length; + while (i--) { + node = firstChilds[i]; + if (!node.hasChildNodes() || (node.firstChild === node.lastChild && node.firstChild.nodeValue === '')) { + dom.remove(node); + } else { + if (isEmptyAnchor(node)) { + dom.remove(node); + } + } + } +}; + +const normalizeZwspOffset = function (start, container, offset) { + if (NodeType.isText(container) === false) { + return offset; + } else if (start) { + return offset === 1 && container.data.charAt(offset - 1) === Zwsp.ZWSP ? 0 : offset; + } else { + return offset === container.data.length - 1 && container.data.charAt(offset) === Zwsp.ZWSP ? container.data.length : offset; + } +}; + +const includeZwspInRange = function (rng) { + const newRng = rng.cloneRange(); + newRng.setStart(rng.startContainer, normalizeZwspOffset(true, rng.startContainer, rng.startOffset)); + newRng.setEnd(rng.endContainer, normalizeZwspOffset(false, rng.endContainer, rng.endOffset)); + return newRng; +}; + +// Trims any linebreaks at the beginning of node user for example when pressing enter in a PRE element +const trimLeadingLineBreaks = function (node) { + do { + if (NodeType.isText(node)) { + node.nodeValue = node.nodeValue.replace(/^[\r\n]+/, ''); + } + + node = node.firstChild; + } while (node); +}; + +const getEditableRoot = function (dom, node) { + const root = dom.getRoot(); + let parent, editableRoot; + + // Get all parents until we hit a non editable parent or the root + parent = node; + while (parent !== root && dom.getContentEditable(parent) !== 'false') { + if (dom.getContentEditable(parent) === 'true') { + editableRoot = parent; + } + + parent = parent.parentNode; + } + + return parent !== root ? editableRoot : root; +}; + +const setForcedBlockAttrs = function (editor, node) { + const forcedRootBlockName = Settings.getForcedRootBlock(editor); + + if (forcedRootBlockName && forcedRootBlockName.toLowerCase() === node.tagName.toLowerCase()) { + editor.dom.setAttribs(node, Settings.getForcedRootBlockAttrs(editor)); + } +}; + +// Wraps any text nodes or inline elements in the specified forced root block name +const wrapSelfAndSiblingsInDefaultBlock = function (editor, newBlockName, rng, container, offset) { + let newBlock, parentBlock, startNode, node, next, rootBlockName; + const blockName = newBlockName || 'P'; + const dom = editor.dom, editableRoot = getEditableRoot(dom, container); + + // Not in a block element or in a table cell or caption + parentBlock = dom.getParent(container, dom.isBlock); + if (!parentBlock || !canSplitBlock(dom, parentBlock)) { + parentBlock = parentBlock || editableRoot; + + if (parentBlock === editor.getBody() || isTableCell(parentBlock)) { + rootBlockName = parentBlock.nodeName.toLowerCase(); + } else { + rootBlockName = parentBlock.parentNode.nodeName.toLowerCase(); + } + + if (!parentBlock.hasChildNodes()) { + newBlock = dom.create(blockName); + setForcedBlockAttrs(editor, newBlock); + parentBlock.appendChild(newBlock); + rng.setStart(newBlock, 0); + rng.setEnd(newBlock, 0); + return newBlock; + } + + // Find parent that is the first child of parentBlock + node = container; + while (node.parentNode !== parentBlock) { + node = node.parentNode; + } + + // Loop left to find start node start wrapping at + while (node && !dom.isBlock(node)) { + startNode = node; + node = node.previousSibling; + } + + if (startNode && editor.schema.isValidChild(rootBlockName, blockName.toLowerCase())) { + newBlock = dom.create(blockName); + setForcedBlockAttrs(editor, newBlock); + startNode.parentNode.insertBefore(newBlock, startNode); + + // Start wrapping until we hit a block + node = startNode; + while (node && !dom.isBlock(node)) { + next = node.nextSibling; + newBlock.appendChild(node); + node = next; + } + + // Restore range to it's past location + rng.setStart(container, offset); + rng.setEnd(container, offset); + } + } + + return container; +}; + +// Adds a BR at the end of blocks that only contains an IMG or INPUT since +// these might be floated and then they won't expand the block +const addBrToBlockIfNeeded = function (dom, block) { + let lastChild; + + // IE will render the blocks correctly other browsers needs a BR + block.normalize(); // Remove empty text nodes that got left behind by the extract + + // Check if the block is empty or contains a floated last child + lastChild = block.lastChild; + if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { + dom.add(block, 'br'); + } +}; + +const insert = function (editor, evt) { + let tmpRng, editableRoot, container, offset, parentBlock, shiftKey; + let newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; + const dom = editor.dom; + const schema = editor.schema, nonEmptyElementsMap = schema.getNonEmptyElements(); + const rng = editor.selection.getRng(); + + // Creates a new block element by cloning the current one or creating a new one if the name is specified + // This function will also copy any text formatting from the parent block and add it to the new one + const createNewBlock = function (name?) { + let node = container, block, clonedNode, caretNode; + const textInlineElements = schema.getTextInlineElements(); + + if (name || parentBlockName === 'TABLE' || parentBlockName === 'HR') { + block = dom.create(name || newBlockName); + setForcedBlockAttrs(editor, block); + } else { + block = parentBlock.cloneNode(false); + } + + caretNode = block; + + if (Settings.shouldKeepStyles(editor) === false) { + dom.setAttrib(block, 'style', null); // wipe out any styles that came over with the block + dom.setAttrib(block, 'class', null); + } else { + // Clone any parent styles + do { + if (textInlineElements[node.nodeName]) { + if (isCaretNode(node)) { + continue; + } + + clonedNode = node.cloneNode(false); + dom.setAttrib(clonedNode, 'id', ''); // Remove ID since it needs to be document unique + + if (block.hasChildNodes()) { + clonedNode.appendChild(block.firstChild); + block.appendChild(clonedNode); + } else { + caretNode = clonedNode; + block.appendChild(clonedNode); + } + } + } while ((node = node.parentNode) && node !== editableRoot); + } + + emptyBlock(caretNode); + + return block; + }; + + // Returns true/false if the caret is at the start/end of the parent block element + const isCaretAtStartOrEndOfBlock = function (start?) { + let walker, node, name, normalizedOffset; + + normalizedOffset = normalizeZwspOffset(start, container, offset); + + // Caret is in the middle of a text node like "a|b" + if (NodeType.isText(container) && (start ? normalizedOffset > 0 : normalizedOffset < container.nodeValue.length)) { + return false; + } + + // If after the last element in block node edge case for #5091 + if (container.parentNode === parentBlock && isAfterLastNodeInContainer && !start) { + return true; + } + + // If the caret if before the first element in parentBlock + if (start && NodeType.isElement(container) && container === parentBlock.firstChild) { + return true; + } + + // Caret can be before/after a table or a hr + if (containerAndSiblingName(container, 'TABLE') || containerAndSiblingName(container, 'HR')) { + return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); + } + + // Walk the DOM and look for text nodes or non empty elements + walker = new TreeWalker(container, parentBlock); + + // If caret is in beginning or end of a text block then jump to the next/previous node + if (NodeType.isText(container)) { + if (start && normalizedOffset === 0) { + walker.prev(); + } else if (!start && normalizedOffset === container.nodeValue.length) { + walker.next(); + } + } + + while ((node = walker.current())) { + if (NodeType.isElement(node)) { + // Ignore bogus elements + if (!node.getAttribute('data-mce-bogus')) { + // Keep empty elements liketext|
text|text2
a |
+ if (dom.isEmpty(newBlock)) { + dom.remove(newBlock); + insertNewBlockAfter(); + } else { + NewLineUtils.moveToCaretPosition(editor, newBlock); + } + } + + dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique + + // Allow custom handling of new blocks + editor.fire('NewBlock', { newBlock }); +}; + +export default { + insert +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertBr.ts b/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertBr.ts new file mode 100644 index 000000000..627b3c583 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertBr.ts @@ -0,0 +1,185 @@ +/** + * InsertBr.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 + */ + +import { Fun } from '@ephox/katamari'; +import { Insert, Element } from '@ephox/sugar'; +import CaretFinder from '../caret/CaretFinder'; +import CaretPosition from '../caret/CaretPosition'; +import NodeType from '../dom/NodeType'; +import TreeWalker from '../api/dom/TreeWalker'; +import BoundaryLocation from '../keyboard/BoundaryLocation'; +import InlineUtils from '../keyboard/InlineUtils'; +import NormalizeRange from '../selection/NormalizeRange'; +import { Selection } from '../api/dom/Selection'; + +// Walks the parent block to the right and look for BR elements +const hasRightSideContent = function (schema, container, parentBlock) { + const walker = new TreeWalker(container, parentBlock); + let node; + const nonEmptyElementsMap = schema.getNonEmptyElements(); + + while ((node = walker.next())) { + if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) { + return true; + } + } +}; + +const scrollToBr = function (dom, selection: Selection, brElm) { + // Insert temp marker and scroll to that + const marker = dom.create('span', {}, ' '); + brElm.parentNode.insertBefore(marker, brElm); + selection.scrollIntoView(marker); + dom.remove(marker); +}; + +const moveSelectionToBr = function (dom, selection: Selection, brElm, extraBr) { + const rng = dom.createRng(); + + if (!extraBr) { + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + } else { + rng.setStartBefore(brElm); + rng.setEndBefore(brElm); + } + + selection.setRng(rng); +}; + +const insertBrAtCaret = function (editor, evt) { + // We load the current event in from EnterKey.js when appropriate to heed + // certain event-specific variations such as ctrl-enter in a list + const selection: Selection = editor.selection, dom = editor.dom; + let brElm, extraBr; + const rng = selection.getRng(); + + NormalizeRange.normalize(dom, rng).each(function (normRng) { + rng.setStart(normRng.startContainer, normRng.startOffset); + rng.setEnd(normRng.endContainer, normRng.endOffset); + }); + + let offset = rng.startOffset; + let container = rng.startContainer; + + // Resolve node index + if (container.nodeType === 1 && container.hasChildNodes()) { + const isAfterLastNodeInContainer = offset > container.childNodes.length - 1; + + container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; + if (isAfterLastNodeInContainer && container.nodeType === 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } + } + + let parentBlock = dom.getParent(container, dom.isBlock); + const containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null; + const containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 + + // Enter inside block contained within a LI then split or insert before/after LI + const isControlKey = evt && evt.ctrlKey; + if (containerBlockName === 'LI' && !isControlKey) { + parentBlock = containerBlock; + } + + if (container && container.nodeType === 3 && offset >= container.nodeValue.length) { + // Insert extra BR element at the end block elements + if (!hasRightSideContent(editor.schema, container, parentBlock)) { + brElm = dom.create('br'); + rng.insertNode(brElm); + rng.setStartAfter(brElm); + rng.setEndAfter(brElm); + extraBr = true; + } + } + + brElm = dom.create('br'); + rng.insertNode(brElm); + + scrollToBr(dom, selection, brElm); + moveSelectionToBr(dom, selection, brElm, extraBr); + editor.undoManager.add(); +}; + +const insertBrBefore = function (editor, inline) { + const br = Element.fromTag('br'); + Insert.before(Element.fromDom(inline), br); + editor.undoManager.add(); +}; + +const insertBrAfter = function (editor, inline) { + if (!hasBrAfter(editor.getBody(), inline)) { + Insert.after(Element.fromDom(inline), Element.fromTag('br')); + } + + const br = Element.fromTag('br'); + Insert.after(Element.fromDom(inline), br); + scrollToBr(editor.dom, editor.selection, br.dom()); + moveSelectionToBr(editor.dom, editor.selection, br.dom(), false); + editor.undoManager.add(); +}; + +const isBeforeBr = function (pos) { + return NodeType.isBr(pos.getNode()); +}; + +const hasBrAfter = function (rootNode, startNode) { + if (isBeforeBr(CaretPosition.after(startNode))) { + return true; + } else { + return CaretFinder.nextPosition(rootNode, CaretPosition.after(startNode)).map(function (pos) { + return NodeType.isBr(pos.getNode()); + }).getOr(false); + } +}; + +const isAnchorLink = function (elm) { + return elm && elm.nodeName === 'A' && 'href' in elm; +}; + +const isInsideAnchor = function (location) { + return location.fold( + Fun.constant(false), + isAnchorLink, + isAnchorLink, + Fun.constant(false) + ); +}; + +const readInlineAnchorLocation = function (editor) { + const isInlineTarget = Fun.curry(InlineUtils.isInlineTarget, editor); + const position = CaretPosition.fromRangeStart(editor.selection.getRng()); + return BoundaryLocation.readLocation(isInlineTarget, editor.getBody(), position).filter(isInsideAnchor); +}; + +const insertBrOutsideAnchor = function (editor, location) { + location.fold( + Fun.noop, + Fun.curry(insertBrBefore, editor), + Fun.curry(insertBrAfter, editor), + Fun.noop + ); +}; + +const insert = function (editor, evt?) { + const anchorLocation = readInlineAnchorLocation(editor); + + if (anchorLocation.isSome()) { + anchorLocation.each(Fun.curry(insertBrOutsideAnchor, editor)); + } else { + insertBrAtCaret(editor, evt); + } +}; + +export default { + insert +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertLi.ts b/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertLi.ts new file mode 100644 index 000000000..f07addcfe --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertLi.ts @@ -0,0 +1,115 @@ +/** + * InsertLi.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 + */ + +import NodeType from '../dom/NodeType'; +import NewLineUtils from './NewLineUtils'; + +const hasFirstChild = function (elm, name) { + return elm.firstChild && elm.firstChild.nodeName === name; +}; + +const hasParent = function (elm, parentName) { + return elm && elm.parentNode && elm.parentNode.nodeName === parentName; +}; + +const isListBlock = function (elm) { + return elm && /^(OL|UL|LI)$/.test(elm.nodeName); +}; + +const isNestedList = function (elm) { + return isListBlock(elm) && isListBlock(elm.parentNode); +}; + +const getContainerBlock = function (containerBlock) { + const containerBlockParent = containerBlock.parentNode; + + if (/^(LI|DT|DD)$/.test(containerBlockParent.nodeName)) { + return containerBlockParent; + } + + return containerBlock; +}; + +const isFirstOrLastLi = function (containerBlock, parentBlock, first) { + let node = containerBlock[first ? 'firstChild' : 'lastChild']; + + // Find first/last element since there might be whitespace there + while (node) { + if (NodeType.isElement(node)) { + break; + } + + node = node[first ? 'nextSibling' : 'previousSibling']; + } + + return node === parentBlock; +}; + +// Inserts a block or br before/after or in the middle of a split list of the LI is empty +const insert = function (editor, createNewBlock, containerBlock, parentBlock, newBlockName) { + const dom = editor.dom; + const rng = editor.selection.getRng(); + + if (containerBlock === editor.getBody()) { + return; + } + + if (isNestedList(containerBlock)) { + newBlockName = 'LI'; + } + + let newBlock = newBlockName ? createNewBlock(newBlockName) : dom.create('BR'); + + if (isFirstOrLastLi(containerBlock, parentBlock, true) && isFirstOrLastLi(containerBlock, parentBlock, false)) { + if (hasParent(containerBlock, 'LI')) { + // Nested list is inside a LI + dom.insertAfter(newBlock, getContainerBlock(containerBlock)); + } else { + // Is first and last list item then replace the OL/UL with a text block + dom.replace(newBlock, containerBlock); + } + } else if (isFirstOrLastLi(containerBlock, parentBlock, true)) { + if (hasParent(containerBlock, 'LI')) { + // List nested in an LI then move the list to a new sibling LI + dom.insertAfter(newBlock, getContainerBlock(containerBlock)); + newBlock.appendChild(dom.doc.createTextNode(' ')); // Needed for IE so the caret can be placed + newBlock.appendChild(containerBlock); + } else { + // First LI in list then remove LI and add text block before list + containerBlock.parentNode.insertBefore(newBlock, containerBlock); + } + } else if (isFirstOrLastLi(containerBlock, parentBlock, false)) { + // Last LI in list then remove LI and add text block after list + dom.insertAfter(newBlock, getContainerBlock(containerBlock)); + } else { + // Middle LI in list the split the list and insert a text block in the middle + // Extract after fragment and insert it after the current block + containerBlock = getContainerBlock(containerBlock); + const tmpRng = rng.cloneRange(); + tmpRng.setStartAfter(parentBlock); + tmpRng.setEndAfter(containerBlock); + const fragment = tmpRng.extractContents(); + + if (newBlockName === 'LI' && hasFirstChild(fragment, 'LI')) { + newBlock = fragment.firstChild; + dom.insertAfter(fragment, containerBlock); + } else { + dom.insertAfter(fragment, containerBlock); + dom.insertAfter(newBlock, containerBlock); + } + } + + dom.remove(parentBlock); + NewLineUtils.moveToCaretPosition(editor, newBlock); +}; + +export default { + insert +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertNewLine.ts b/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertNewLine.ts new file mode 100644 index 000000000..635209f35 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/newline/InsertNewLine.ts @@ -0,0 +1,30 @@ +/** + * InsertNewLine.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 + */ + +import { Fun } from '@ephox/katamari'; +import InsertBlock from './InsertBlock'; +import InsertBr from './InsertBr'; +import NewLineAction from './NewLineAction'; + +const insert = function (editor, evt) { + NewLineAction.getAction(editor, evt).fold( + function () { + InsertBr.insert(editor, evt); + }, + function () { + InsertBlock.insert(editor, evt); + }, + Fun.noop + ); +}; + +export default { + insert +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/newline/NewLineAction.ts b/tools-ng/tinymce/editor/src/core/main/ts/newline/NewLineAction.ts new file mode 100644 index 000000000..5eee58de3 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/newline/NewLineAction.ts @@ -0,0 +1,101 @@ +/** + * NewLineAction.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 + */ + +import { Adt, Arr, Option } from '@ephox/katamari'; +import Settings from '../api/Settings'; +import ContextSelectors from './ContextSelectors'; +import NewLineUtils from './NewLineUtils'; +import LazyEvaluator from '../util/LazyEvaluator'; + +const newLineAction = Adt.generate([ + { br: [ ] }, + { block: [ ] }, + { none: [ ] } +]); + +const shouldBlockNewLine = function (editor, shiftKey) { + return ContextSelectors.shouldBlockNewLine(editor); +}; + +const isBrMode = function (requiredState) { + return function (editor, shiftKey) { + const brMode = Settings.getForcedRootBlock(editor) === ''; + return brMode === requiredState; + }; +}; + +const inListBlock = function (requiredState) { + return function (editor, shiftKey) { + return NewLineUtils.isListItemParentBlock(editor) === requiredState; + }; +}; + +const inBlock = (blockName: string, requiredState: boolean) => { + return function (editor, shiftKey) { + const state = NewLineUtils.getParentBlockName(editor) === blockName.toUpperCase(); + return state === requiredState; + }; +}; + +const inPreBlock = (requiredState: boolean) => inBlock('pre', requiredState); +const inSummaryBlock = () => inBlock('summary', true); + +const shouldPutBrInPre = function (requiredState) { + return function (editor, shiftKey) { + return Settings.shouldPutBrInPre(editor) === requiredState; + }; +}; + +const inBrContext = function (editor, shiftKey) { + return ContextSelectors.shouldInsertBr(editor); +}; + +const hasShiftKey = function (editor, shiftKey) { + return shiftKey; +}; + +const canInsertIntoEditableRoot = function (editor) { + const forcedRootBlock = Settings.getForcedRootBlock(editor); + const rootEditable = NewLineUtils.getEditableRoot(editor.dom, editor.selection.getStart()); + + return rootEditable && editor.schema.isValidChild(rootEditable.nodeName, forcedRootBlock ? forcedRootBlock : 'P'); +}; + +const match = function (predicates, action) { + return function (editor, shiftKey) { + const isMatch = Arr.foldl(predicates, function (res, p) { + return res && p(editor, shiftKey); + }, true); + + return isMatch ? Option.some(action) : Option.none(); + }; +}; + +const getAction = function (editor, evt) { + return LazyEvaluator.evaluateUntil([ + match([shouldBlockNewLine], newLineAction.none()), + match([inSummaryBlock()], newLineAction.br()), + match([inPreBlock(true), shouldPutBrInPre(false), hasShiftKey], newLineAction.br()), + match([inPreBlock(true), shouldPutBrInPre(false)], newLineAction.block()), + match([inPreBlock(true), shouldPutBrInPre(true), hasShiftKey], newLineAction.block()), + match([inPreBlock(true), shouldPutBrInPre(true)], newLineAction.br()), + match([inListBlock(true), hasShiftKey], newLineAction.br()), + match([inListBlock(true)], newLineAction.block()), + match([isBrMode(true), hasShiftKey, canInsertIntoEditableRoot], newLineAction.block()), + match([isBrMode(true)], newLineAction.br()), + match([inBrContext], newLineAction.br()), + match([isBrMode(false), hasShiftKey], newLineAction.br()), + match([canInsertIntoEditableRoot], newLineAction.block()) + ], [editor, evt.shiftKey]).getOr(newLineAction.none()); +}; + +export default { + getAction +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/newline/NewLineUtils.ts b/tools-ng/tinymce/editor/src/core/main/ts/newline/NewLineUtils.ts new file mode 100644 index 000000000..9f7bd87e2 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/newline/NewLineUtils.ts @@ -0,0 +1,136 @@ +/** + * NewLineUtils.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 + */ + +import { Fun, Option } from '@ephox/katamari'; +import { Element } from '@ephox/sugar'; +import * as ElementType from '../dom/ElementType'; +import NodeType from '../dom/NodeType'; +import TreeWalker from '../api/dom/TreeWalker'; + +const firstNonWhiteSpaceNodeSibling = function (node) { + while (node) { + if (node.nodeType === 1 || (node.nodeType === 3 && node.data && /[\r\n\s]/.test(node.data))) { + return node; + } + + node = node.nextSibling; + } +}; + +const moveToCaretPosition = function (editor, root) { + // tslint:disable-next-line:prefer-const + let walker, node, rng, lastNode = root, tempElm; + const dom = editor.dom; + const moveCaretBeforeOnEnterElementsMap = editor.schema.getMoveCaretBeforeOnEnterElements(); + + if (!root) { + return; + } + + if (/^(LI|DT|DD)$/.test(root.nodeName)) { + const firstChild = firstNonWhiteSpaceNodeSibling(root.firstChild); + + if (firstChild && /^(UL|OL|DL)$/.test(firstChild.nodeName)) { + root.insertBefore(dom.doc.createTextNode('\u00a0'), root.firstChild); + } + } + + rng = dom.createRng(); + root.normalize(); + + if (root.hasChildNodes()) { + walker = new TreeWalker(root, root); + + while ((node = walker.current())) { + if (NodeType.isText(node)) { + rng.setStart(node, 0); + rng.setEnd(node, 0); + break; + } + + if (moveCaretBeforeOnEnterElementsMap[node.nodeName.toLowerCase()]) { + rng.setStartBefore(node); + rng.setEndBefore(node); + break; + } + + lastNode = node; + node = walker.next(); + } + + if (!node) { + rng.setStart(lastNode, 0); + rng.setEnd(lastNode, 0); + } + } else { + if (NodeType.isBr(root)) { + if (root.nextSibling && dom.isBlock(root.nextSibling)) { + rng.setStartBefore(root); + rng.setEndBefore(root); + } else { + rng.setStartAfter(root); + rng.setEndAfter(root); + } + } else { + rng.setStart(root, 0); + rng.setEnd(root, 0); + } + } + + editor.selection.setRng(rng); + + // Remove tempElm created for old IE:s + dom.remove(tempElm); + editor.selection.scrollIntoView(root); +}; + +const getEditableRoot = function (dom, node) { + const root = dom.getRoot(); + let parent, editableRoot; + + // Get all parents until we hit a non editable parent or the root + parent = node; + while (parent !== root && dom.getContentEditable(parent) !== 'false') { + if (dom.getContentEditable(parent) === 'true') { + editableRoot = parent; + } + + parent = parent.parentNode; + } + + return parent !== root ? editableRoot : root; +}; + +const getParentBlock = function (editor) { + return Option.from(editor.dom.getParent(editor.selection.getStart(true), editor.dom.isBlock)); +}; + +const getParentBlockName = function (editor) { + return getParentBlock(editor).fold( + Fun.constant(''), + function (parentBlock) { + return parentBlock.nodeName.toUpperCase(); + } + ); +}; + +const isListItemParentBlock = function (editor) { + return getParentBlock(editor).filter(function (elm) { + return ElementType.isListItem(Element.fromDom(elm)); + }).isSome(); +}; + +export default { + moveToCaretPosition, + getEditableRoot, + getParentBlock, + getParentBlockName, + isListItemParentBlock +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/CaretRangeFromPoint.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/CaretRangeFromPoint.ts new file mode 100644 index 000000000..6505ae02c --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/CaretRangeFromPoint.ts @@ -0,0 +1,106 @@ +/** + * CaretRangeFromPoint.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 + */ + +import NodeType from '../dom/NodeType'; +import Tools from '../api/util/Tools'; +import { Document, Range } from '@ephox/dom-globals'; + +const hasCeProperty = function (node) { + return NodeType.isContentEditableTrue(node) || NodeType.isContentEditableFalse(node); +}; + +const findParent = function (node, rootNode, predicate) { + while (node && node !== rootNode) { + if (predicate(node)) { + return node; + } + + node = node.parentNode; + } + + return null; +}; + +/** + * Finds the closest selection rect tries to get the range from that. + */ +const findClosestIeRange = function (clientX, clientY, doc) { + let element, rng, rects; + + element = doc.elementFromPoint(clientX, clientY); + rng = doc.body.createTextRange(); + + if (!element || element.tagName === 'HTML') { + element = doc.body; + } + + rng.moveToElementText(element); + rects = Tools.toArray(rng.getClientRects()); + + rects = rects.sort(function (a, b) { + a = Math.abs(Math.max(a.top - clientY, a.bottom - clientY)); + b = Math.abs(Math.max(b.top - clientY, b.bottom - clientY)); + + return a - b; + }); + + if (rects.length > 0) { + clientY = (rects[0].bottom + rects[0].top) / 2; + + try { + rng.moveToPoint(clientX, clientY); + rng.collapse(true); + + return rng; + } catch (ex) { + // At least we tried + } + } + + return null; +}; + +const moveOutOfContentEditableFalse = function (rng, rootNode) { + const parentElement = rng && rng.parentElement ? rng.parentElement() : null; + return NodeType.isContentEditableFalse(findParent(parentElement, rootNode, hasCeProperty)) ? null : rng; +}; + +const fromPoint = function (clientX: number, clientY: number, doc: Document): Range { + let rng, point; + const pointDoc = doc as any; + + if (pointDoc.caretPositionFromPoint) { + point = pointDoc.caretPositionFromPoint(clientX, clientY); + if (point) { + rng = doc.createRange(); + rng.setStart(point.offsetNode, point.offset); + rng.collapse(true); + } + } else if (doc.caretRangeFromPoint) { + rng = doc.caretRangeFromPoint(clientX, clientY); + } else if (pointDoc.body.createTextRange) { + rng = pointDoc.body.createTextRange(); + + try { + rng.moveToPoint(clientX, clientY); + rng.collapse(true); + } catch (ex) { + rng = findClosestIeRange(clientX, clientY, doc); + } + + return moveOutOfContentEditableFalse(rng, doc.body); + } + + return rng; +}; + +export default { + fromPoint +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/DetailsElement.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/DetailsElement.ts new file mode 100644 index 000000000..7c3b6be09 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/DetailsElement.ts @@ -0,0 +1,47 @@ +/** + * DetailsElement.js + * + * Released under LGPL License. + * Copyright (c) 1999-2018 Ephox Corp. All rights reserved + * + * License: http://www.tinymce.com/license + * Contributing: http://www.tinymce.com/contributing + */ + +import { Editor } from 'tinymce/core/api/Editor'; +import { Arr, Type } from '@ephox/katamari'; + +const preventSummaryToggle = (editor: Editor) => { + editor.on('click', (e) => { + if (editor.dom.getParent(e.target, 'details')) { + e.preventDefault(); + } + }); +}; + +// Forces the details element to always be open within the editor +const filterDetails = (editor: Editor) => { + editor.parser.addNodeFilter('details', function (elms) { + Arr.each(elms, function (details) { + details.attr('data-mce-open', details.attr('open')); + details.attr('open', 'open'); + }); + }); + + editor.serializer.addNodeFilter('details', function (elms) { + Arr.each(elms, function (details) { + const open = details.attr('data-mce-open'); + details.attr('open', Type.isString(open) ? open : null); + details.attr('data-mce-open', null); + }); + }); +}; + +const setup = (editor: Editor) => { + preventSummaryToggle(editor); + filterDetails(editor); +}; + +export { + setup +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/ElementSelection.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/ElementSelection.ts new file mode 100644 index 000000000..e3ef61e39 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/ElementSelection.ts @@ -0,0 +1,156 @@ +/** + * ElementSelection.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 + */ + +import { Option } from '@ephox/katamari'; +import { Node as SugarNode, Traverse, Element as SugarElement } from '@ephox/sugar'; +import TreeWalker from '../api/dom/TreeWalker'; +import { moveEndPoint } from './SelectionUtils'; +import NodeType from '../dom/NodeType'; +import { Element, Range, Node } from '@ephox/dom-globals'; +import { DOMUtils } from 'tinymce/core/api/dom/DOMUtils'; + +const getEndpointElement = (root: Element, rng: Range, start: boolean, real: boolean, resolve: (elm, offset: number) => number) => { + const container = start ? rng.startContainer : rng.endContainer; + const offset = start ? rng.startOffset : rng.endOffset; + + return Option.from(container).map(SugarElement.fromDom).map((elm) => { + return !real || !rng.collapsed ? Traverse.child(elm, resolve(elm, offset)).getOr(elm) : elm; + }).bind((elm) => SugarNode.isElement(elm) ? Option.some(elm) : Traverse.parent(elm)).map((elm: any) => elm.dom()).getOr(root); +}; + +const getStart = (root: Element, rng: Range, real?: boolean): Element => { + return getEndpointElement(root, rng, true, real, (elm, offset) => Math.min(Traverse.childNodesCount(elm), offset)); +}; + +const getEnd = (root: Element, rng: Range, real?: boolean): Element => { + return getEndpointElement(root, rng, false, real, (elm, offset) => offset > 0 ? offset - 1 : offset); +}; + +const skipEmptyTextNodes = function (node: Node, forwards: boolean) { + const orig = node; + + while (node && NodeType.isText(node) && node.length === 0) { + node = forwards ? node.nextSibling : node.previousSibling; + } + + return node || orig; +}; + +const getNode = (root: Element, rng: Range): Element => { + let elm, startContainer, endContainer, startOffset, endOffset; + + // Range maybe lost after the editor is made visible again + if (!rng) { + return root; + } + + startContainer = rng.startContainer; + endContainer = rng.endContainer; + startOffset = rng.startOffset; + endOffset = rng.endOffset; + elm = rng.commonAncestorContainer; + + // Handle selection a image or other control like element such as anchors + if (!rng.collapsed) { + if (startContainer === endContainer) { + if (endOffset - startOffset < 2) { + if (startContainer.hasChildNodes()) { + elm = startContainer.childNodes[startOffset]; + } + } + } + + // If the anchor node is a element instead of a text node then return this element + // if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) + // return sel.anchorNode.childNodes[sel.anchorOffset]; + + // Handle cases where the selection is immediately wrapped around a node and return that node instead of it's parent. + // This happens when you double click an underlined word in FireFox. + if (startContainer.nodeType === 3 && endContainer.nodeType === 3) { + if (startContainer.length === startOffset) { + startContainer = skipEmptyTextNodes(startContainer.nextSibling, true); + } else { + startContainer = startContainer.parentNode; + } + + if (endOffset === 0) { + endContainer = skipEmptyTextNodes(endContainer.previousSibling, false); + } else { + endContainer = endContainer.parentNode; + } + + if (startContainer && startContainer === endContainer) { + return startContainer; + } + } + } + + if (elm && elm.nodeType === 3) { + return elm.parentNode; + } + + return elm; +}; + +const getSelectedBlocks = (dom: DOMUtils, rng: Range, startElm?: Element, endElm?: Element): Element[] => { + let node, root; + const selectedBlocks = []; + + root = dom.getRoot(); + startElm = dom.getParent(startElm || getStart(root, rng, rng.collapsed), dom.isBlock) as Element; + endElm = dom.getParent(endElm || getEnd(root, rng, rng.collapsed), dom.isBlock) as Element; + + if (startElm && startElm !== root) { + selectedBlocks.push(startElm); + } + + if (startElm && endElm && startElm !== endElm) { + node = startElm; + + const walker = new TreeWalker(startElm, root); + while ((node = walker.next()) && node !== endElm) { + if (dom.isBlock(node)) { + selectedBlocks.push(node); + } + } + } + + if (endElm && startElm !== endElm && endElm !== root) { + selectedBlocks.push(endElm); + } + + return selectedBlocks; +}; + +const select = (dom, node: Node, content?: boolean) => { + return Option.from(node).map((node) => { + const idx = dom.nodeIndex(node); + const rng = dom.createRng(); + + rng.setStart(node.parentNode, idx); + rng.setEnd(node.parentNode, idx + 1); + + // Find first/last text node or BR element + if (content) { + moveEndPoint(dom, rng, node, true); + moveEndPoint(dom, rng, node, false); + } + + return rng; + }); +}; + +export { + getStart, + getEnd, + getNode, + getSelectedBlocks, + select +}; diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/EventProcessRanges.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/EventProcessRanges.ts new file mode 100644 index 000000000..d86e1b35c --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/EventProcessRanges.ts @@ -0,0 +1,23 @@ +/** + * EventProcessRanges.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 + */ + +import { Arr } from '@ephox/katamari'; +import { Range } from '@ephox/dom-globals'; + +const processRanges = (editor, ranges: Range[]): Range[] => { + return Arr.map(ranges, (range) => { + const evt = editor.fire('GetSelectionRange', { range }); + return evt.range !== range ? evt.range : range; + }); +}; + +export default { + processRanges +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/FragmentReader.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/FragmentReader.ts new file mode 100644 index 000000000..06a9db9d2 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/FragmentReader.ts @@ -0,0 +1,108 @@ +/** + * FragmentReader.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 + */ + +import { Arr, Fun } from '@ephox/katamari'; +import { Compare, Insert, Replication, Element, Fragment, Node, SelectorFind, Traverse } from '@ephox/sugar'; +import * as ElementType from '../dom/ElementType'; +import Parents from '../dom/Parents'; +import * as SelectionUtils from './SelectionUtils'; +import SimpleTableModel from './SimpleTableModel'; +import TableCellSelection from './TableCellSelection'; + +const findParentListContainer = function (parents) { + return Arr.find(parents, function (elm) { + return Node.name(elm) === 'ul' || Node.name(elm) === 'ol'; + }); +}; + +const getFullySelectedListWrappers = function (parents, rng) { + return Arr.find(parents, function (elm) { + return Node.name(elm) === 'li' && SelectionUtils.hasAllContentsSelected(elm, rng); + }).fold( + Fun.constant([]), + function (li) { + return findParentListContainer(parents).map(function (listCont) { + return [ + Element.fromTag('li'), + Element.fromTag(Node.name(listCont)) + ]; + }).getOr([]); + } + ); +}; + +const wrap = function (innerElm, elms) { + const wrapped = Arr.foldl(elms, function (acc, elm) { + Insert.append(elm, acc); + return elm; + }, innerElm); + return elms.length > 0 ? Fragment.fromElements([wrapped]) : wrapped; +}; + +const directListWrappers = function (commonAnchorContainer) { + if (ElementType.isListItem(commonAnchorContainer)) { + return Traverse.parent(commonAnchorContainer).filter(ElementType.isList).fold( + Fun.constant([]), + function (listElm) { + return [ commonAnchorContainer, listElm ]; + } + ); + } else { + return ElementType.isList(commonAnchorContainer) ? [ commonAnchorContainer ] : [ ]; + } +}; + +const getWrapElements = function (rootNode, rng) { + const commonAnchorContainer = Element.fromDom(rng.commonAncestorContainer); + const parents = Parents.parentsAndSelf(commonAnchorContainer, rootNode); + const wrapElements = Arr.filter(parents, function (elm) { + return ElementType.isInline(elm) || ElementType.isHeading(elm); + }); + const listWrappers = getFullySelectedListWrappers(parents, rng); + const allWrappers = wrapElements.concat(listWrappers.length ? listWrappers : directListWrappers(commonAnchorContainer)); + return Arr.map(allWrappers, Replication.shallow); +}; + +const emptyFragment = function () { + return Fragment.fromElements([]); +}; + +const getFragmentFromRange = function (rootNode, rng) { + return wrap(Element.fromDom(rng.cloneContents()), getWrapElements(rootNode, rng)); +}; + +const getParentTable = function (rootElm, cell) { + return SelectorFind.ancestor(cell, 'table', Fun.curry(Compare.eq, rootElm)); +}; + +const getTableFragment = function (rootNode, selectedTableCells) { + return getParentTable(rootNode, selectedTableCells[0]).bind(function (tableElm) { + const firstCell = selectedTableCells[0]; + const lastCell = selectedTableCells[selectedTableCells.length - 1]; + const fullTableModel = SimpleTableModel.fromDom(tableElm); + + return SimpleTableModel.subsection(fullTableModel, firstCell, lastCell).map(function (sectionedTableModel) { + return Fragment.fromElements([SimpleTableModel.toDom(sectionedTableModel)]); + }); + }).getOrThunk(emptyFragment); +}; + +const getSelectionFragment = function (rootNode, ranges) { + return ranges.length > 0 && ranges[0].collapsed ? emptyFragment() : getFragmentFromRange(rootNode, ranges[0]); +}; + +const read = function (rootNode, ranges) { + const selectedCells = TableCellSelection.getCellsFromElementOrRanges(ranges, rootNode); + return selectedCells.length > 0 ? getTableFragment(rootNode, selectedCells) : getSelectionFragment(rootNode, ranges); +}; + +export default { + read +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/GetSelectionContent.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/GetSelectionContent.ts new file mode 100644 index 000000000..1782b4502 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/GetSelectionContent.ts @@ -0,0 +1,68 @@ +/** + * GetSelectionContent.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 + */ + +import { Element } from '@ephox/sugar'; +import EventProcessRanges from './EventProcessRanges'; +import FragmentReader from './FragmentReader'; +import MultiRange from './MultiRange'; +import Zwsp from '../text/Zwsp'; + +const getContent = function (editor, args) { + const rng = editor.selection.getRng(), tmpElm = editor.dom.create('body'); + const sel = editor.selection.getSel(); + let fragment; + const ranges = EventProcessRanges.processRanges(editor, MultiRange.getRanges(sel)); + + args = args || {}; + args.get = true; + args.format = args.format || 'html'; + args.selection = true; + + args = editor.fire('BeforeGetContent', args); + if (args.isDefaultPrevented()) { + editor.fire('GetContent', args); + return args.content; + } + + if (args.format === 'text') { + return editor.selection.isCollapsed() ? '' : Zwsp.trim(rng.text || (sel.toString ? sel.toString() : '')); + } + + if (rng.cloneContents) { + fragment = args.contextual ? FragmentReader.read(Element.fromDom(editor.getBody()), ranges).dom() : rng.cloneContents(); + if (fragment) { + tmpElm.appendChild(fragment); + } + } else if (rng.item !== undefined || rng.htmlText !== undefined) { + // IE will produce invalid markup if elements are present that + // it doesn't understand like custom elements or HTML5 elements. + // Adding a BR in front of the contents and then remoiving it seems to fix it though. + tmpElm.innerHTML = '
|
|
[a
x|
a
b
+// It would become this range in webkit: +//[a
]b
+// We would want it to be: +//[a]
b
+// Since it would otherwise produces spans out of thin air on insertContent for example. +const normalizeBlockSelectionRange = (rng: Range): Range => { + const startPos = CaretPosition.fromRangeStart(rng); + const endPos = CaretPosition.fromRangeEnd(rng); + const rootNode = rng.commonAncestorContainer; + + return CaretFinder.fromPosition(false, rootNode, endPos) + .map(function (newEndPos) { + if (!CaretUtils.isInSameBlock(startPos, endPos, rootNode) && CaretUtils.isInSameBlock(startPos, newEndPos, rootNode)) { + return createRange(startPos.container(), startPos.offset(), newEndPos.container(), newEndPos.offset()); + } else { + return rng; + } + }).getOr(rng); +}; + +const normalize = (rng: Range): Range => rng.collapsed ? rng : normalizeBlockSelectionRange(rng); + +export default { + normalize +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/RangeWalk.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/RangeWalk.ts new file mode 100644 index 000000000..71be206d5 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/RangeWalk.ts @@ -0,0 +1,179 @@ +/** + * RangeWalk.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 + */ + +import Tools from '../api/util/Tools'; + +const each = Tools.each; + +const getEndChild = function (container, index) { + const childNodes = container.childNodes; + + index--; + + if (index > childNodes.length - 1) { + index = childNodes.length - 1; + } else if (index < 0) { + index = 0; + } + + return childNodes[index] || container; +}; + +const walk = function (dom, rng, callback) { + let startContainer = rng.startContainer; + const startOffset = rng.startOffset; + let endContainer = rng.endContainer; + const endOffset = rng.endOffset; + let ancestor; + let startPoint; + let endPoint; + let node; + let parent; + let siblings; + let nodes; + + // Handle table cell selection the table plugin enables + // you to fake select table cells and perform formatting actions on them + nodes = dom.select('td[data-mce-selected],th[data-mce-selected]'); + if (nodes.length > 0) { + each(nodes, function (node) { + callback([node]); + }); + + return; + } + + /** + * Excludes start/end text node if they are out side the range + * + * @private + * @param {Array} nodes Nodes to exclude items from. + * @return {Array} Array with nodes excluding the start/end container if needed. + */ + const exclude = function (nodes) { + let node; + + // First node is excluded + node = nodes[0]; + if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) { + nodes.splice(0, 1); + } + + // Last node is excluded + node = nodes[nodes.length - 1]; + if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) { + nodes.splice(nodes.length - 1, 1); + } + + return nodes; + }; + + const collectSiblings = function (node, name, endNode?) { + const siblings = []; + + for (; node && node !== endNode; node = node[name]) { + siblings.push(node); + } + + return siblings; + }; + + const findEndPoint = function (node, root) { + do { + if (node.parentNode === root) { + return node; + } + + node = node.parentNode; + } while (node); + }; + + const walkBoundary = function (startNode, endNode, next?) { + const siblingName = next ? 'nextSibling' : 'previousSibling'; + + for (node = startNode, parent = node.parentNode; node && node !== endNode; node = parent) { + parent = node.parentNode; + siblings = collectSiblings(node === startNode ? node : node[siblingName], siblingName); + + if (siblings.length) { + if (!next) { + siblings.reverse(); + } + + callback(exclude(siblings)); + } + } + }; + + // If index based start position then resolve it + if (startContainer.nodeType === 1 && startContainer.hasChildNodes()) { + startContainer = startContainer.childNodes[startOffset]; + } + + // If index based end position then resolve it + if (endContainer.nodeType === 1 && endContainer.hasChildNodes()) { + endContainer = getEndChild(endContainer, endOffset); + } + + // Same container + if (startContainer === endContainer) { + return callback(exclude([startContainer])); + } + + // Find common ancestor and end points + ancestor = dom.findCommonAncestor(startContainer, endContainer); + + // Process left side + for (node = startContainer; node; node = node.parentNode) { + if (node === endContainer) { + return walkBoundary(startContainer, ancestor, true); + } + + if (node === ancestor) { + break; + } + } + + // Process right side + for (node = endContainer; node; node = node.parentNode) { + if (node === startContainer) { + return walkBoundary(endContainer, ancestor); + } + + if (node === ancestor) { + break; + } + } + + // Find start/end point + startPoint = findEndPoint(startContainer, ancestor) || startContainer; + endPoint = findEndPoint(endContainer, ancestor) || endContainer; + + // Walk left leaf + walkBoundary(startContainer, startPoint, true); + + // Walk the middle from start to end point + siblings = collectSiblings( + startPoint === startContainer ? startPoint : startPoint.nextSibling, + 'nextSibling', + endPoint === endContainer ? endPoint.nextSibling : endPoint + ); + + if (siblings.length) { + callback(exclude(siblings)); + } + + // Walk right leaf + walkBoundary(endContainer, endPoint); +}; + +export default { + walk +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionBookmark.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionBookmark.ts new file mode 100644 index 000000000..86cb530ec --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionBookmark.ts @@ -0,0 +1,125 @@ +import { Fun, Option } from '@ephox/katamari'; +import { PlatformDetection } from '@ephox/sand'; +import { Compare, Element, Node, Text, Traverse, Selection } from '@ephox/sugar'; +import { document } from '@ephox/dom-globals'; + +const browser = PlatformDetection.detect().browser; + +const clamp = function (offset, element) { + const max = Node.isText(element) ? Text.get(element).length : Traverse.children(element).length + 1; + + if (offset > max) { + return max; + } else if (offset < 0) { + return 0; + } + + return offset; +}; + +const normalizeRng = function (rng) { + return Selection.range( + rng.start(), + clamp(rng.soffset(), rng.start()), + rng.finish(), + clamp(rng.foffset(), rng.finish()) + ); +}; + +const isOrContains = function (root, elm) { + return Compare.contains(root, elm) || Compare.eq(root, elm); +}; + +const isRngInRoot = function (root) { + return function (rng) { + return isOrContains(root, rng.start()) && isOrContains(root, rng.finish()); + }; +}; + +// var dumpRng = function (rng) { +// console.log('start', rng.start().dom()); +// console.log('soffset', rng.soffset()); +// console.log('finish', rng.finish().dom()); +// console.log('foffset', rng.foffset()); +// return rng; +// }; + +const shouldStore = function (editor) { + return editor.inline === true || browser.isIE(); +}; + +const nativeRangeToSelectionRange = function (r) { + return Selection.range(Element.fromDom(r.startContainer), r.startOffset, Element.fromDom(r.endContainer), r.endOffset); +}; + +const readRange = function (win) { + const selection = win.getSelection(); + const rng = !selection || selection.rangeCount === 0 ? Option.none() : Option.from(selection.getRangeAt(0)); + return rng.map(nativeRangeToSelectionRange); +}; + +const getBookmark = function (root) { + const win = Traverse.defaultView(root); + + return readRange(win.dom()) + .filter(isRngInRoot(root)); +}; + +const validate = function (root, bookmark) { + return Option.from(bookmark) + .filter(isRngInRoot(root)) + .map(normalizeRng); +}; + +const bookmarkToNativeRng = function (bookmark) { + const rng = document.createRange(); + + try { + // Might throw IndexSizeError + rng.setStart(bookmark.start().dom(), bookmark.soffset()); + rng.setEnd(bookmark.finish().dom(), bookmark.foffset()); + return Option.some(rng); + } catch (_) { + return Option.none(); + } +}; + +const store = function (editor) { + const newBookmark = shouldStore(editor) ? getBookmark(Element.fromDom(editor.getBody())) : Option.none(); + + editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark; +}; + +const storeNative = function (editor, rng) { + const root = Element.fromDom(editor.getBody()); + const range = shouldStore(editor) ? Option.from(rng) : Option.none(); + + const newBookmark = range.map(nativeRangeToSelectionRange) + .filter(isRngInRoot(root)); + + editor.bookmark = newBookmark.isSome() ? newBookmark : editor.bookmark; +}; + +const getRng = function (editor) { + const bookmark = editor.bookmark ? editor.bookmark : Option.none(); + + return bookmark + .bind(Fun.curry(validate, Element.fromDom(editor.getBody()))) + .bind(bookmarkToNativeRng); +}; + +const restore = function (editor) { + getRng(editor).each(function (rng) { + editor.selection.setRng(rng); + }); +}; + +export default { + store, + storeNative, + readRange, + restore, + getRng, + getBookmark, + validate +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionRestore.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionRestore.ts new file mode 100644 index 000000000..d7f1eaf1d --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionRestore.ts @@ -0,0 +1,81 @@ +/** + * SelectionRestore.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 + */ + +import { Throttler } from '@ephox/katamari'; +import { PlatformDetection } from '@ephox/sand'; +import DOMUtils from '../api/dom/DOMUtils'; +import SelectionBookmark from './SelectionBookmark'; +import { document } from '@ephox/dom-globals'; + +const isManualNodeChange = function (e) { + return e.type === 'nodechange' && e.selectionChange; +}; + +const registerPageMouseUp = function (editor, throttledStore) { + const mouseUpPage = function () { + throttledStore.throttle(); + }; + + DOMUtils.DOM.bind(document, 'mouseup', mouseUpPage); + + editor.on('remove', function () { + DOMUtils.DOM.unbind(document, 'mouseup', mouseUpPage); + }); +}; + +const registerFocusOut = function (editor) { + editor.on('focusout', function () { + SelectionBookmark.store(editor); + }); +}; + +const registerMouseUp = function (editor, throttledStore) { + editor.on('mouseup touchend', function (e) { + throttledStore.throttle(); + }); +}; + +const registerEditorEvents = function (editor, throttledStore) { + const browser = PlatformDetection.detect().browser; + + if (browser.isIE()) { + registerFocusOut(editor); + } else { + registerMouseUp(editor, throttledStore); + } + + editor.on('keyup nodechange', function (e) { + if (!isManualNodeChange(e)) { + SelectionBookmark.store(editor); + } + }); +}; + +const register = function (editor) { + const throttledStore = Throttler.first(function () { + SelectionBookmark.store(editor); + }, 0); + + if (editor.inline) { + registerPageMouseUp(editor, throttledStore); + } + + editor.on('init', function () { + registerEditorEvents(editor, throttledStore); + }); + + editor.on('remove', function () { + throttledStore.cancel(); + }); +}; + +export default { + register +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionUtils.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionUtils.ts new file mode 100644 index 000000000..bd0c0162a --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/SelectionUtils.ts @@ -0,0 +1,132 @@ +/** + * SelectionUtils.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 + */ + +import { Arr, Fun, Option, Options } from '@ephox/katamari'; +import { Compare, Element, Node, Traverse } from '@ephox/sugar'; +import NodeType from '../dom/NodeType'; +import Env from '../api/Env'; +import TreeWalker from '../api/dom/TreeWalker'; +import Tools from '../api/util/Tools'; +import { Editor } from 'tinymce/core/api/Editor'; +import { Range } from '@ephox/dom-globals'; + +const getStartNode = function (rng) { + const sc = rng.startContainer, so = rng.startOffset; + if (NodeType.isText(sc)) { + return so === 0 ? Option.some(Element.fromDom(sc)) : Option.none(); + } else { + return Option.from(sc.childNodes[so]).map(Element.fromDom); + } +}; + +const getEndNode = function (rng) { + const ec = rng.endContainer, eo = rng.endOffset; + if (NodeType.isText(ec)) { + return eo === ec.data.length ? Option.some(Element.fromDom(ec)) : Option.none(); + } else { + return Option.from(ec.childNodes[eo - 1]).map(Element.fromDom); + } +}; + +const getFirstChildren = function (node) { + return Traverse.firstChild(node).fold( + Fun.constant([node]), + function (child) { + return [node].concat(getFirstChildren(child)); + } + ); +}; + +const getLastChildren = function (node) { + return Traverse.lastChild(node).fold( + Fun.constant([node]), + function (child) { + if (Node.name(child) === 'br') { + return Traverse.prevSibling(child).map(function (sibling) { + return [node].concat(getLastChildren(sibling)); + }).getOr([]); + } else { + return [node].concat(getLastChildren(child)); + } + } + ); +}; + +const hasAllContentsSelected = function (elm, rng) { + return Options.liftN([getStartNode(rng), getEndNode(rng)], function (startNode, endNode) { + const start = Arr.find(getFirstChildren(elm), Fun.curry(Compare.eq, startNode)); + const end = Arr.find(getLastChildren(elm), Fun.curry(Compare.eq, endNode)); + return start.isSome() && end.isSome(); + }).getOr(false); +}; + +const moveEndPoint = (dom, rng: Range, node, start: boolean): void => { + const root = node, walker = new TreeWalker(node, root); + const nonEmptyElementsMap = dom.schema.getNonEmptyElements(); + + do { + // Text node + if (node.nodeType === 3 && Tools.trim(node.nodeValue).length !== 0) { + if (start) { + rng.setStart(node, 0); + } else { + rng.setEnd(node, node.nodeValue.length); + } + + return; + } + + // BR/IMG/INPUT elements but not table cells + if (nonEmptyElementsMap[node.nodeName] && !/^(TD|TH)$/.test(node.nodeName)) { + if (start) { + rng.setStartBefore(node); + } else { + if (node.nodeName === 'BR') { + rng.setEndBefore(node); + } else { + rng.setEndAfter(node); + } + } + + return; + } + + // Found empty text block old IE can place the selection inside those + if (Env.ie && Env.ie < 11 && dom.isBlock(node) && dom.isEmpty(node)) { + if (start) { + rng.setStart(node, 0); + } else { + rng.setEnd(node, 0); + } + + return; + } + } while ((node = (start ? walker.next() : walker.prev()))); + + // Failed to find any text node or other suitable location then move to the root of body + if (root.nodeName === 'BODY') { + if (start) { + rng.setStart(root, 0); + } else { + rng.setEnd(root, root.childNodes.length); + } + } +}; + +const hasAnyRanges = (editor: Editor) => { + const sel = editor.selection.getSel(); + return sel && sel.rangeCount > 0; +}; + +export { + hasAllContentsSelected, + moveEndPoint, + hasAnyRanges +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/SetSelectionContent.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/SetSelectionContent.ts new file mode 100644 index 000000000..994a1cdd4 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/SetSelectionContent.ts @@ -0,0 +1,103 @@ +/** + * SetSelectionContent.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 + */ + +const setContent = function (editor, content, args) { + let rng = editor.selection.getRng(), caretNode; + const doc = editor.getDoc(); + let frag, temp; + + args = args || { format: 'html' }; + args.set = true; + args.selection = true; + args.content = content; + + // Dispatch before set content event + if (!args.no_events) { + args = editor.fire('BeforeSetContent', args); + if (args.isDefaultPrevented()) { + editor.fire('SetContent', args); + return; + } + } + + content = args.content; + + if (rng.insertNode) { + // Make caret marker since insertNode places the caret in the beginning of text after insert + content += '_'; + + // Delete and insert new node + if (rng.startContainer === doc && rng.endContainer === doc) { + // WebKit will fail if the body is empty since the range is then invalid and it can't insert contents + doc.body.innerHTML = content; + } else { + rng.deleteContents(); + + if (doc.body.childNodes.length === 0) { + doc.body.innerHTML = content; + } else { + // createContextualFragment doesn't exists in IE 9 DOMRanges + if (rng.createContextualFragment) { + rng.insertNode(rng.createContextualFragment(content)); + } else { + // Fake createContextualFragment call in IE 9 + frag = doc.createDocumentFragment(); + temp = doc.createElement('div'); + + frag.appendChild(temp); + temp.outerHTML = content; + + rng.insertNode(frag); + } + } + } + + // Move to caret marker + caretNode = editor.dom.get('__caret'); + + // Make sure we wrap it compleatly, Opera fails with a simple select call + rng = doc.createRange(); + rng.setStartBefore(caretNode); + rng.setEndBefore(caretNode); + editor.selection.setRng(rng); + + // Remove the caret position + editor.dom.remove('__caret'); + + try { + editor.selection.setRng(rng); + } catch (ex) { + // Might fail on Opera for some odd reason + } + } else { + if (rng.item) { + // Delete content and get caret text selection + doc.execCommand('Delete', false, null); + rng = editor.getRng(); + } + + // Explorer removes spaces from the beginning of pasted contents + if (/^\s+/.test(content)) { + rng.pasteHTML('_' + content); + editor.dom.remove('__mce_tmp'); + } else { + rng.pasteHTML(content); + } + } + + // Dispatch set content event + if (!args.no_events) { + editor.fire('SetContent', args); + } +}; + +export default { + setContent +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/SimpleTableModel.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/SimpleTableModel.ts new file mode 100644 index 000000000..b02bf58c6 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/SimpleTableModel.ts @@ -0,0 +1,151 @@ +/** + * SimpleTableModel.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 + */ + +import { Arr, Option, Struct } from '@ephox/katamari'; +import { Compare, Insert, InsertAll, Replication, Element, Attr, SelectorFilter } from '@ephox/sugar'; + +const tableModel = Struct.immutable('element', 'width', 'rows'); +const tableRow = Struct.immutable('element', 'cells'); +const cellPosition = Struct.immutable('x', 'y'); + +const getSpan = function (td, key) { + const value = parseInt(Attr.get(td, key), 10); + return isNaN(value) ? 1 : value; +}; + +const fillout = function (table, x, y, tr, td) { + const rowspan = getSpan(td, 'rowspan'); + const colspan = getSpan(td, 'colspan'); + const rows = table.rows(); + + for (let y2 = y; y2 < y + rowspan; y2++) { + if (!rows[y2]) { + rows[y2] = tableRow(Replication.deep(tr), []); + } + + for (let x2 = x; x2 < x + colspan; x2++) { + const cells = rows[y2].cells(); + + // not filler td:s are purposely not cloned so that we can + // find cells in the model by element object references + cells[x2] = y2 === y && x2 === x ? td : Replication.shallow(td); + } + } +}; + +const cellExists = function (table, x, y) { + const rows = table.rows(); + const cells = rows[y] ? rows[y].cells() : []; + return !!cells[x]; +}; + +const skipCellsX = function (table, x, y) { + while (cellExists(table, x, y)) { + x++; + } + + return x; +}; + +const getWidth = function (rows) { + return Arr.foldl(rows, function (acc, row) { + return row.cells().length > acc ? row.cells().length : acc; + }, 0); +}; + +const findElementPos = function (table, element) { + const rows = table.rows(); + for (let y = 0; y < rows.length; y++) { + const cells = rows[y].cells(); + for (let x = 0; x < cells.length; x++) { + if (Compare.eq(cells[x], element)) { + return Option.some(cellPosition(x, y)); + } + } + } + + return Option.none(); +}; + +const extractRows = function (table, sx, sy, ex, ey) { + const newRows = []; + const rows = table.rows(); + + for (let y = sy; y <= ey; y++) { + const cells = rows[y].cells(); + const slice = sx < ex ? cells.slice(sx, ex + 1) : cells.slice(ex, sx + 1); + newRows.push(tableRow(rows[y].element(), slice)); + } + + return newRows; +}; + +const subTable = function (table, startPos, endPos) { + const sx = startPos.x(), sy = startPos.y(); + const ex = endPos.x(), ey = endPos.y(); + const newRows = sy < ey ? extractRows(table, sx, sy, ex, ey) : extractRows(table, sx, ey, ex, sy); + + return tableModel(table.element(), getWidth(newRows), newRows); +}; + +const createDomTable = function (table, rows) { + const tableElement = Replication.shallow(table.element()); + const tableBody = Element.fromTag('tbody'); + + InsertAll.append(tableBody, rows); + Insert.append(tableElement, tableBody); + + return tableElement; +}; + +const modelRowsToDomRows = function (table) { + return Arr.map(table.rows(), function (row) { + const cells = Arr.map(row.cells(), function (cell) { + const td = Replication.deep(cell); + Attr.remove(td, 'colspan'); + Attr.remove(td, 'rowspan'); + return td; + }); + + const tr = Replication.shallow(row.element()); + InsertAll.append(tr, cells); + return tr; + }); +}; + +const fromDom = function (tableElm) { + const table = tableModel(Replication.shallow(tableElm), 0, []); + + Arr.each(SelectorFilter.descendants(tableElm, 'tr'), function (tr, y) { + Arr.each(SelectorFilter.descendants(tr, 'td,th'), function (td, x) { + fillout(table, skipCellsX(table, x, y), y, tr, td); + }); + }); + + return tableModel(table.element(), getWidth(table.rows()), table.rows()); +}; + +const toDom = function (table) { + return createDomTable(table, modelRowsToDomRows(table)); +}; + +const subsection = function (table, startElement, endElement) { + return findElementPos(table, startElement).bind(function (startPos) { + return findElementPos(table, endElement).map(function (endPos) { + return subTable(table, startPos, endPos); + }); + }); +}; + +export default { + fromDom, + toDom, + subsection +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/SplitRange.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/SplitRange.ts new file mode 100644 index 000000000..31b6ed97e --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/SplitRange.ts @@ -0,0 +1,62 @@ +/** + * SplitRange.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 + */ + +import NodeType from '../dom/NodeType'; + +const splitText = function (node, offset) { + return node.splitText(offset); +}; + +const split = function (rng) { + let startContainer = rng.startContainer, + startOffset = rng.startOffset, + endContainer = rng.endContainer, + endOffset = rng.endOffset; + + // Handle single text node + if (startContainer === endContainer && NodeType.isText(startContainer)) { + if (startOffset > 0 && startOffset < startContainer.nodeValue.length) { + endContainer = splitText(startContainer, startOffset); + startContainer = endContainer.previousSibling; + + if (endOffset > startOffset) { + endOffset = endOffset - startOffset; + startContainer = endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + startOffset = 0; + } else { + endOffset = 0; + } + } + } else { + // Split startContainer text node if needed + if (NodeType.isText(startContainer) && startOffset > 0 && startOffset < startContainer.nodeValue.length) { + startContainer = splitText(startContainer, startOffset); + startOffset = 0; + } + + // Split endContainer text node if needed + if (NodeType.isText(endContainer) && endOffset > 0 && endOffset < endContainer.nodeValue.length) { + endContainer = splitText(endContainer, endOffset).previousSibling; + endOffset = endContainer.nodeValue.length; + } + } + + return { + startContainer, + startOffset, + endContainer, + endOffset + }; +}; + +export default { + split +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/TableCellSelection.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/TableCellSelection.ts new file mode 100644 index 000000000..fc33b5271 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/TableCellSelection.ts @@ -0,0 +1,40 @@ +/** + * TableCellSelection.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 + */ + +import { Arr } from '@ephox/katamari'; +import { Element, SelectorFilter } from '@ephox/sugar'; +import * as ElementType from '../dom/ElementType'; +import MultiRange from './MultiRange'; + +const getCellsFromRanges = function (ranges) { + return Arr.filter(MultiRange.getSelectedNodes(ranges), ElementType.isTableCell); +}; + +const getCellsFromElement = function (elm) { + const selectedCells = SelectorFilter.descendants(elm, 'td[data-mce-selected],th[data-mce-selected]'); + return selectedCells; +}; + +const getCellsFromElementOrRanges = function (ranges, element) { + const selectedCells = getCellsFromElement(element); + const rangeCells = getCellsFromRanges(ranges); + return selectedCells.length > 0 ? selectedCells : rangeCells; +}; + +const getCellsFromEditor = function (editor) { + return getCellsFromElementOrRanges(MultiRange.getRanges(editor.selection.getSel()), Element.fromDom(editor.getBody())); +}; + +export default { + getCellsFromRanges, + getCellsFromElement, + getCellsFromElementOrRanges, + getCellsFromEditor +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/selection/WordSelection.ts b/tools-ng/tinymce/editor/src/core/main/ts/selection/WordSelection.ts new file mode 100644 index 000000000..d57c965c0 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/selection/WordSelection.ts @@ -0,0 +1,46 @@ +/** + * WordSelection.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 + */ + +import { Type } from '@ephox/katamari'; +import * as CaretContainer from '../caret/CaretContainer'; +import CaretPosition from '../caret/CaretPosition'; +import { Selection } from '../api/dom/Selection'; +import { Editor } from 'tinymce/core/api/Editor'; + +const hasSelectionModifyApi = function (editor: Editor) { + return Type.isFunction((|
+ * + * Or: + *|
+ if (!isDefaultPrevented(e) && (keyCode === DELETE || keyCode === BACKSPACE)) { + isCollapsed = editor.selection.isCollapsed(); + body = editor.getBody(); + + // Selection is collapsed but the editor isn't empty + if (isCollapsed && !dom.isEmpty(body)) { + return; + } + + // Selection isn't collapsed but not all the contents is selected + if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { + return; + } + + // Manually empty the editor + e.preventDefault(); + editor.setContent(''); + + if (body.firstChild && dom.isBlock(body.firstChild)) { + editor.selection.setCursorLocation(body.firstChild, 0); + } else { + editor.selection.setCursorLocation(body, 0); + } + + editor.nodeChanged(); + } + }); + }; + + /** + * WebKit doesn't select all the nodes in the body when you press Ctrl+A. + * IE selects more than the contents [a
] instead of[a]
see bug #6438 + * This selects the whole body so that backspace/delete logic will delete everything + */ + const selectAll = function () { + editor.shortcuts.add('meta+a', null, 'SelectAll'); + }; + + /** + * WebKit has a weird issue where it some times fails to properly convert keypresses to input method keystrokes. + * The IME on Mac doesn't initialize when it doesn't fire a proper focus event. + * + * This seems to happen when the user manages to click the documentElement element then the window doesn't get proper focus until + * you enter a character into the editor. + * + * It also happens when the first focus in made to the body. + * + * See: https://bugs.webkit.org/show_bug.cgi?id=83566 + */ + const inputMethodFocus = function () { + if (!editor.settings.content_editable) { + // Case 1 IME doesn't initialize if you focus the document + // Disabled since it was interferring with the cE=false logic + // Also coultn't reproduce the issue on Safari 9 + /*dom.bind(editor.getDoc(), 'focusin', function() { + selection.setRng(selection.getRng()); + });*/ + + // Case 2 IME doesn't initialize if you click the documentElement it also doesn't properly fire the focusin event + // Needs to be both down/up due to weird rendering bug on Chrome Windows + dom.bind(editor.getDoc(), 'mousedown mouseup', function (e) { + let rng; + + if (e.target === editor.getDoc().documentElement) { + rng = selection.getRng(); + editor.getBody().focus(); + + if (e.type === 'mousedown') { + if (CaretContainer.isCaretContainer(rng.startContainer)) { + return; + } + + // Edge case for mousedown, drag select and mousedown again within selection on Chrome Windows to render caret + selection.placeCaretAt(e.clientX, e.clientY); + } else { + selection.setRng(rng); + } + } + }); + } + }; + + /** + * Backspacing in FireFox/IE from a paragraph into a horizontal rule results in a floating text node because the + * browser just deletes the paragraph - the browser fails to merge the text node with a horizontal rule so it is + * left there. TinyMCE sees a floating text node and wraps it in a paragraph on the key up event (ForceBlocks.js + * addRootBlocks), meaning the action does nothing. With this code, FireFox/IE matche the behaviour of other + * browsers. + * + * It also fixes a bug on Firefox where it's impossible to delete HR elements. + */ + const removeHrOnBackspace = function () { + editor.on('keydown', function (e) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { + // Check if there is any HR elements this is faster since getRng on IE 7 & 8 is slow + if (!editor.getBody().getElementsByTagName('hr').length) { + return; + } + + if (selection.isCollapsed() && selection.getRng().startOffset === 0) { + const node = selection.getNode(); + const previousSibling = node.previousSibling; + + if (node.nodeName === 'HR') { + dom.remove(node); + e.preventDefault(); + return; + } + + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === 'hr') { + dom.remove(previousSibling); + e.preventDefault(); + } + } + } + }); + }; + + /** + * Firefox 3.x has an issue where the body element won't get proper focus if you click out + * side it's rectangle. + */ + const focusBody = function () { + // Fix for a focus bug in FF 3.x where the body element + // wouldn't get proper focus if the user clicked on the HTML element + if (!Range.prototype.getClientRects) { // Detect getClientRects got introduced in FF 4 + editor.on('mousedown', function (e) { + if (!isDefaultPrevented(e) && e.target.nodeName === 'HTML') { + const body = editor.getBody(); + + // Blur the body it's focused but not correctly focused + body.blur(); + + // Refocus the body after a little while + Delay.setEditorTimeout(editor, function () { + body.focus(); + }); + } + }); + } + }; + + /** + * WebKit has a bug where it isn't possible to select image, hr or anchor elements + * by clicking on them so we need to fake that. + */ + const selectControlElements = function () { + editor.on('click', function (e) { + const target = e.target; + + // Workaround for bug, http://bugs.webkit.org/show_bug.cgi?id=12250 + // WebKit can't even do simple things like selecting an image + // Needs to be the setBaseAndExtend or it will fail to select floated images + if (/^(IMG|HR)$/.test(target.nodeName) && dom.getContentEditableParent(target) !== 'false') { + e.preventDefault(); + editor.selection.select(target); + editor.nodeChanged(); + } + + if (target.nodeName === 'A' && dom.hasClass(target, 'mce-item-anchor')) { + e.preventDefault(); + selection.select(target); + } + }); + }; + + /** + * Fixes a Gecko bug where the style attribute gets added to the wrong element when deleting between two block elements. + * + * Fixes do backspace/delete on this: + *bla[ck
r]ed
+ * + * Would become: + *bla|ed
+ * + * Instead of: + *bla|ed
+ */ + const removeStylesWhenDeletingAcrossBlockElements = function () { + const getAttributeApplyFunction = function () { + const template = dom.getAttribs(selection.getStart().cloneNode(false)); + + return function () { + const target = selection.getStart(); + + if (target !== editor.getBody()) { + dom.setAttrib(target, 'style', null); + + each(template, function (attr) { + target.setAttributeNode(attr.cloneNode(true)); + }); + } + }; + }; + + const isSelectionAcrossElements = function () { + return !selection.isCollapsed() && + dom.getParent(selection.getStart(), dom.isBlock) !== dom.getParent(selection.getEnd(), dom.isBlock); + }; + + editor.on('keypress', function (e) { + let applyAttributes; + + if (!isDefaultPrevented(e) && (e.keyCode === 8 || e.keyCode === 46) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + editor.getDoc().execCommand('delete', false, null); + applyAttributes(); + e.preventDefault(); + return false; + } + }); + + dom.bind(editor.getDoc(), 'cut', function (e) { + let applyAttributes; + + if (!isDefaultPrevented(e) && isSelectionAcrossElements()) { + applyAttributes = getAttributeApplyFunction(); + + Delay.setEditorTimeout(editor, function () { + applyAttributes(); + }); + } + }); + }; + + /** + * Backspacing into a table behaves differently depending upon browser type. + * Therefore, disable Backspace when cursor immediately follows a table. + */ + const disableBackspaceIntoATable = function () { + editor.on('keydown', function (e) { + if (!isDefaultPrevented(e) && e.keyCode === BACKSPACE) { + if (selection.isCollapsed() && selection.getRng().startOffset === 0) { + const previousSibling = selection.getNode().previousSibling; + if (previousSibling && previousSibling.nodeName && previousSibling.nodeName.toLowerCase() === 'table') { + e.preventDefault(); + return false; + } + } + } + }); + }; + + /** + * Removes a blockquote when backspace is pressed at the beginning of it. + * + * For example: + *+ * + * Becomes: + *|x
|x
+ */ + const removeBlockQuoteOnBackSpace = function () { + // Add block quote deletion handler + editor.on('keydown', function (e) { + let rng, container, offset, root, parent; + + if (isDefaultPrevented(e) || e.keyCode !== VK.BACKSPACE) { + return; + } + + rng = selection.getRng(); + container = rng.startContainer; + offset = rng.startOffset; + root = dom.getRoot(); + parent = container; + + if (!rng.collapsed || offset !== 0) { + return; + } + + while (parent && parent.parentNode && parent.parentNode.firstChild === parent && parent.parentNode !== root) { + parent = parent.parentNode; + } + + // Is the cursor at the beginning of a blockquote? + if (parent.tagName === 'BLOCKQUOTE') { + // Remove the blockquote + editor.formatter.toggle('blockquote', null, parent); + + // Move the caret to the beginning of container + rng = dom.createRng(); + rng.setStart(container, 0); + rng.setEnd(container, 0); + selection.setRng(rng); + } + }); + }; + + /** + * Sets various Gecko editing options on mouse down and before a execCommand to disable inline table editing that is broken etc. + */ + const setGeckoEditingOptions = function () { + const setOpts = function () { + refreshContentEditable(); + + setEditorCommandState('StyleWithCSS', false); + setEditorCommandState('enableInlineTableEditing', false); + + if (!settings.object_resizing) { + setEditorCommandState('enableObjectResizing', false); + } + }; + + if (!settings.readonly) { + editor.on('BeforeExecCommand MouseDown', setOpts); + } + }; + + /** + * Fixes a gecko link bug, when a link is placed at the end of block elements there is + * no way to move the caret behind the link. This fix adds a bogus br element after the link. + * + * For example this: + * + * + * Becomes this: + * + */ + const addBrAfterLastLinks = function () { + const fixLinks = function () { + each(dom.select('a'), function (node) { + let parentNode = node.parentNode; + const root = dom.getRoot(); + + if (parentNode.lastChild === node) { + while (parentNode && !dom.isBlock(parentNode)) { + if (parentNode.parentNode.lastChild !== parentNode || parentNode === root) { + return; + } + + parentNode = parentNode.parentNode; + } + + dom.add(parentNode, 'br', { 'data-mce-bogus': 1 }); + } + }); + }; + + editor.on('SetContent ExecCommand', function (e) { + if (e.type === 'setcontent' || e.command === 'mceInsertLink') { + fixLinks(); + } + }); + }; + + /** + * WebKit will produce DIV elements here and there by default. But since TinyMCE uses paragraphs by + * default we want to change that behavior. + */ + const setDefaultBlockType = function () { + if (settings.forced_root_block) { + editor.on('init', function () { + setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); + }); + } + }; + + /** + * Fixes selection issues where the caret can be placed between two inline elements like a|b + * this fix will lean the caret right into the closest inline element. + */ + const normalizeSelection = function () { + // Normalize selection for example a|a becomes a|a + editor.on('keyup focusin mouseup', function (e) { + // no point to exclude Ctrl+A, since normalization will still run after Ctrl will be unpressed + // better exclude any key combinations with the modifiers to avoid double normalization + // (also addresses TINY-1130) + if (!VK.modifierPressed(e)) { + selection.normalize(); + } + }, true); + }; + + /** + * Forces Gecko to render a broken image icon if it fails to load an image. + */ + const showBrokenImageIcon = function () { + editor.contentStyles.push( + 'img:-moz-broken {' + + '-moz-force-broken-image-icon:1;' + + 'min-width:24px;' + + 'min-height:24px' + + '}' + ); + }; + + /** + * iOS has a bug where it's impossible to type if the document has a touchstart event + * bound and the user touches the document while having the on screen keyboard visible. + * + * The touch event moves the focus to the parent document while having the caret inside the iframe + * this fix moves the focus back into the iframe document. + */ + const restoreFocusOnKeyDown = function () { + if (!editor.inline) { + editor.on('keydown', function () { + if (document.activeElement === document.body) { + editor.getWin().focus(); + } + }); + } + }; + + /** + * IE 11 has an annoying issue where you can't move focus into the editor + * by clicking on the white area HTML element. We used to be able to fix this with + * the fixCaretSelectionOfDocumentElementOnIe fix. But since M$ removed the selection + * object it's not possible anymore. So we need to hack in a ungly CSS to force the + * body to be at least 150px. If the user clicks the HTML element out side this 150px region + * we simply move the focus into the first paragraph. Not ideal since you loose the + * positioning of the caret but goot enough for most cases. + */ + const bodyHeight = function () { + if (!editor.inline) { + editor.contentStyles.push('body {min-height: 150px}'); + editor.on('click', function (e) { + let rng; + + if (e.target.nodeName === 'HTML') { + // Edge seems to only need focus if we set the range + // the caret will become invisible and moved out of the iframe!! + if (Env.ie > 11) { + editor.getBody().focus(); + return; + } + + // Need to store away non collapsed ranges since the focus call will mess that up see #7382 + rng = editor.selection.getRng(); + editor.getBody().focus(); + editor.selection.setRng(rng); + editor.selection.normalize(); + editor.nodeChanged(); + } + }); + } + }; + + /** + * Firefox on Mac OS will move the browser back to the previous page if you press CMD+Left arrow. + * You might then loose all your work so we need to block that behavior and replace it with our own. + */ + const blockCmdArrowNavigation = function () { + if (Env.mac) { + editor.on('keydown', function (e) { + if (VK.metaKeyPressed(e) && !e.shiftKey && (e.keyCode === 37 || e.keyCode === 39)) { + e.preventDefault(); + editor.selection.getSel().modify('move', e.keyCode === 37 ? 'backward' : 'forward', 'lineboundary'); + } + }); + } + }; + + /** + * Disables the autolinking in IE 9+ this is then re-enabled by the autolink plugin. + */ + const disableAutoUrlDetect = function () { + setEditorCommandState('AutoUrlDetect', false); + }; + + /** + * iOS 7.1 introduced two new bugs: + * 1) It's possible to open links within a contentEditable area by clicking on them. + * 2) If you hold down the finger it will display the link/image touch callout menu. + */ + const tapLinksAndImages = function () { + editor.on('click', function (e) { + let elm = e.target; + + do { + if (elm.tagName === 'A') { + e.preventDefault(); + return; + } + } while ((elm = elm.parentNode)); + }); + + editor.contentStyles.push('.mce-content-body {-webkit-touch-callout: none}'); + }; + + /** + * iOS Safari and possible other browsers have a bug where it won't fire + * a click event when a contentEditable is focused. This function fakes click events + * by using touchstart/touchend and measuring the time and distance travelled. + */ + /* + function touchClickEvent() { + editor.on('touchstart', function(e) { + var elm, time, startTouch, changedTouches; + + elm = e.target; + time = new Date().getTime(); + changedTouches = e.changedTouches; + + if (!changedTouches || changedTouches.length > 1) { + return; + } + + startTouch = changedTouches[0]; + + editor.once('touchend', function(e) { + var endTouch = e.changedTouches[0], args; + + if (new Date().getTime() - time > 500) { + return; + } + + if (Math.abs(startTouch.clientX - endTouch.clientX) > 5) { + return; + } + + if (Math.abs(startTouch.clientY - endTouch.clientY) > 5) { + return; + } + + args = { + target: elm + }; + + each('pageX pageY clientX clientY screenX screenY'.split(' '), function(key) { + args[key] = endTouch[key]; + }); + + args = editor.fire('click', args); + + if (!args.isDefaultPrevented()) { + // iOS WebKit can't place the caret properly once + // you bind touch events so we need to do this manually + // TODO: Expand to the closest word? Touble tap still works. + editor.selection.placeCaretAt(endTouch.clientX, endTouch.clientY); + editor.nodeChanged(); + } + }); + }); + } + */ + + /** + * WebKit has a bug where it will allow forms to be submitted if they are inside a contentEditable element. + * For example this: + */ + const blockFormSubmitInsideEditor = function () { + editor.on('init', function () { + editor.dom.bind(editor.getBody(), 'submit', function (e) { + e.preventDefault(); + }); + }); + }; + + /** + * Sometimes WebKit/Blink generates BR elements with the Apple-interchange-newline class. + * + * Scenario: + * 1) Create a table 2x2. + * 2) Select and copy cells A2-B2. + * 3) Paste and it will add BR element to table cell. + */ + const removeAppleInterchangeBrs = function () { + parser.addNodeFilter('br', function (nodes) { + let i = nodes.length; + + while (i--) { + if (nodes[i].attr('class') === 'Apple-interchange-newline') { + nodes[i].remove(); + } + } + }); + }; + + /** + * IE cannot set custom contentType's on drag events, and also does not properly drag/drop between + * editors. This uses a special data:text/mce-internal URL to pass data when drag/drop between editors. + */ + const ieInternalDragAndDrop = function () { + editor.on('dragstart', function (e) { + setMceInternalContent(e); + }); + + editor.on('drop', function (e) { + if (!isDefaultPrevented(e)) { + const internalContent = getMceInternalContent(e); + + if (internalContent && internalContent.id !== editor.id) { + e.preventDefault(); + + const rng = CaretRangeFromPoint.fromPoint(e.x, e.y, editor.getDoc()); + selection.setRng(rng); + insertClipboardContents(internalContent.html, true); + } + } + }); + }; + + const refreshContentEditable = function () { + // No-op since Mozilla seems to have fixed the caret repaint issues + }; + + const isHidden = function () { + let sel; + + if (!isGecko || editor.removed) { + return 0; + } + + // Weird, wheres that cursor selection? + sel = editor.selection.getSel(); + return (!sel || !sel.rangeCount || sel.rangeCount === 0); + }; + + // All browsers + removeBlockQuoteOnBackSpace(); + emptyEditorWhenDeleting(); + + // Windows phone will return a range like [body, 0] on mousedown so + // it will always normalize to the wrong location + if (!Env.windowsPhone) { + normalizeSelection(); + } + + // WebKit + if (isWebKit) { + inputMethodFocus(); + selectControlElements(); + setDefaultBlockType(); + blockFormSubmitInsideEditor(); + disableBackspaceIntoATable(); + removeAppleInterchangeBrs(); + + // touchClickEvent(); + + // iOS + if (Env.iOS) { + restoreFocusOnKeyDown(); + bodyHeight(); + tapLinksAndImages(); + } else { + selectAll(); + } + } + + if (Env.ie >= 11) { + bodyHeight(); + disableBackspaceIntoATable(); + } + + if (Env.ie) { + selectAll(); + disableAutoUrlDetect(); + ieInternalDragAndDrop(); + } + + // Gecko + if (isGecko) { + removeHrOnBackspace(); + focusBody(); + removeStylesWhenDeletingAcrossBlockElements(); + setGeckoEditingOptions(); + addBrAfterLastLinks(); + showBrokenImageIcon(); + blockCmdArrowNavigation(); + disableBackspaceIntoATable(); + } + + return { + refreshContentEditable, + isHidden + }; +} \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/main/ts/util/Uuid.ts b/tools-ng/tinymce/editor/src/core/main/ts/util/Uuid.ts new file mode 100644 index 000000000..ba2400330 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/main/ts/util/Uuid.ts @@ -0,0 +1,35 @@ +/** + * Uuid.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 + */ + +/** + * Generates unique ids. + * + * @class tinymce.util.Uuid + * @private + */ + +let count = 0; + +const seed = function () { + const rnd = function () { + return Math.round(Math.random() * 0xFFFFFFFF).toString(36); + }; + + const now = new Date().getTime(); + return 's' + now.toString(36) + rnd() + rnd() + rnd(); +}; + +const uuid = function (prefix) { + return prefix + (count++) + seed(); +}; + +export default { + uuid +}; \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/test/css/ui-overrides.css b/tools-ng/tinymce/editor/src/core/test/css/ui-overrides.css new file mode 100644 index 000000000..cea0bf431 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/css/ui-overrides.css @@ -0,0 +1,34 @@ +/* Hardcodes sizes since fonts vary on platforms */ + +.ui-overrides .mce-spacer { + width: 20px; + height: 20px; + visibility: visible; + border: 0 solid black; +} + +.ui-overrides .mce-head .mce-title { + width: 100px; + height: 20px; + display: inline-block; +} + +.ui-overrides .mce-btn .mce-txt { + display: inline-block; + width: 10px; +} + +/* Colors used for debugging */ + +.ui-overrides .mce-red {background-color: red;} +.ui-overrides .mce-green {background-color: green;} +.ui-overrides .mce-blue {background-color: blue;} +.ui-overrides .mce-yellow {background-color: yellow;} +.ui-overrides .mce-magenta {background-color: magenta;} +.ui-overrides .mce-cyan {background-color: cyan;} +.ui-overrides .mce-dotted {background-image: url(data:image/gif;base64,R0lGODlhCwALAIABAAAAAP///yH5BAEAAAEALAAAAAALAAsAAAIPjI+py+CuHkwSRHkf23wXADs=);} +.ui-overrides .mce-i-test {background: red;} + +body .ui-overrides .mce-window, body .ui-overrides .mce-notification { + transform: none; +} diff --git a/tools-ng/tinymce/editor/src/core/test/json/routes.json b/tools-ng/tinymce/editor/src/core/test/json/routes.json new file mode 100644 index 000000000..5708f5449 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/json/routes.json @@ -0,0 +1,87 @@ +[ + { + "request": { + "method": "get", + "path": "/custom/json_rpc_ok" + }, + + "response": { + "json": { + "result": "Hello JSON-RPC", + "error": null, + "id": 1 + } + } + }, + + { + "request": { + "method": "get", + "path": "/custom/json_rpc_fail" + }, + + "response": { + "json": { + "result": null, + "error": { + "message": "General failure", + "code": 42 + }, + "id": 1 + } + } + }, + + { + "request": { + "method": "get", + "path": "/custom/404" + }, + + "response": { + "status": 404, + "json": {} + } + }, + + { + "request": { + "method": "get", + "path": "/custom/403" + }, + "response": { + "status": 403, + "json": {} + } + }, + + { + "request": { + "method": "get", + "path": "/custom/403data" + }, + "response": { + "status": 403, + "json": { + "error": { + "type": "error" + } + } + } + }, + + { + "request": { + "method": "post", + "path": "/custom/imageUpload" + }, + + "response": { + "status" : 200, + "json": { + "location": "uploaded_image.jpg" + } + + } + } +] \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/test/ts/.eslintrc b/tools-ng/tinymce/editor/src/core/test/ts/.eslintrc new file mode 100644 index 000000000..8dda5126a --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/.eslintrc @@ -0,0 +1,21 @@ +{ + "env": { + "browser": false, + "amd": true + }, + + "globals": { + "assert": true, + "test": true, + "asynctest": true + }, + + "rules": { + "eqeqeq": "error", + "space-before-function-paren": ["error", "always"], + "yoda": "error", + "func-style": ["error", "expression"] + }, + + "extends": "../../../../../.eslintrc" +} diff --git a/tools-ng/tinymce/editor/src/core/test/ts/atomic/keyboard/MatchKeysTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/atomic/keyboard/MatchKeysTest.ts new file mode 100644 index 000000000..8cce964c6 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/atomic/keyboard/MatchKeysTest.ts @@ -0,0 +1,102 @@ +import { Assertions, Logger, Pipeline, Step } from '@ephox/agar'; +import { Arr, Cell, Merger } from '@ephox/katamari'; +import MatchKeys from 'tinymce/core/keyboard/MatchKeys'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('atomic.tinymce.core.keyboard.MatchKeysTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const state = Cell([]); + + const event = function (evt) { + return Merger.merge({ + shiftKey: false, + altKey: false, + ctrlKey: false, + metaKey: false, + keyCode: 0 + }, evt); + }; + + const handleAction = function (value) { + return function () { + state.set(state.get().concat([value])); + return true; + }; + }; + + const sTestMatch = function (patterns, event, expectedData) { + return Step.sync(function () { + state.set([]); + + const matches = MatchKeys.match(patterns, event); + Assertions.assertEq('Should have some matches', true, matches.length > 0); + + Arr.find(matches, function (pattern) { + return pattern.action(); + }); + + Assertions.assertEq('Should have the expected state', expectedData, state.get()); + }); + }; + + const sTestMatchNone = function (patterns, event) { + return Step.sync(function () { + Assertions.assertEq( + 'Should not produce any matches', + 0, + MatchKeys.match(patterns, event).length + ); + }); + }; + + const sTestExecute = function (patterns, event, expectedData, expectedMatch) { + return Step.sync(function () { + state.set([]); + + const result = MatchKeys.execute(patterns, event); + Assertions.assertEq('Should be expected match', expectedMatch, result.getOrDie()); + Assertions.assertEq('Should have the expected state', expectedData, state.get()); + }); + }; + + const actionA = handleAction('a'); + const actionB = handleAction('b'); + + Pipeline.async({}, [ + sTestMatchNone([], {}), + sTestMatchNone([], event({ keyCode: 65 })), + sTestMatchNone([{ keyCode: 65, action: actionA }], event({ keyCode: 13 })), + sTestMatch([{ keyCode: 65, action: actionA }], event({ keyCode: 65 }), ['a']), + sTestMatch([{ keyCode: 65, shiftKey: true, action: actionA }], event({ keyCode: 65, shiftKey: true }), ['a']), + sTestMatch([{ keyCode: 65, altKey: true, action: actionA }], event({ keyCode: 65, altKey: true }), ['a']), + sTestMatch([{ keyCode: 65, ctrlKey: true, action: actionA }], event({ keyCode: 65, ctrlKey: true }), ['a']), + sTestMatch([{ keyCode: 65, metaKey: true, action: actionA }], event({ keyCode: 65, metaKey: true }), ['a']), + sTestMatch( + [ + { keyCode: 65, ctrlKey: true, metaKey: true, altKey: true, action: actionA }, + { keyCode: 65, ctrlKey: true, metaKey: true, action: actionB } + ], + event({ keyCode: 65, metaKey: true, ctrlKey: true }), + ['b'] + ), + sTestExecute( + [ + { keyCode: 65, ctrlKey: true, metaKey: true, altKey: true, action: actionA }, + { keyCode: 65, ctrlKey: true, metaKey: true, action: actionB } + ], + event({ keyCode: 65, metaKey: true, ctrlKey: true }), + ['b'], + { shiftKey: false, altKey: false, ctrlKey: true, metaKey: true, keyCode: 65, action: actionB } + ), + Logger.t('Action wrapper helper', Step.sync(function () { + const action = MatchKeys.action(function () { + return Array.prototype.slice.call(arguments, 0); + }, 1, 2, 3); + + Assertions.assertEq('Should return the parameters passed in', [1, 2, 3], action()); + })) + ], function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/atomic/keyboard/NbspsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/atomic/keyboard/NbspsTest.ts new file mode 100644 index 000000000..dfb999fbb --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/atomic/keyboard/NbspsTest.ts @@ -0,0 +1,19 @@ +import { RawAssertions } from '@ephox/agar'; +import { UnitTest } from '@ephox/bedrock'; +import { normalizeNbspMiddle } from 'tinymce/core/keyboard/Nbsps'; + +UnitTest.test('atomic.tinymce.core.keyboard.NbspsTest', () => { + RawAssertions.assertEq('Should remain unchanged empty string', '', normalizeNbspMiddle('')); + RawAssertions.assertEq('Should remain unchanged single letter', 'a', normalizeNbspMiddle('a')); + RawAssertions.assertEq('Should remain unchanged two letters', 'ab', normalizeNbspMiddle('ab')); + RawAssertions.assertEq('Should remain unchanged three letters', 'abc', normalizeNbspMiddle('abc')); + RawAssertions.assertEq('Should remain unchanged nbsp at start', '\u00a0a', normalizeNbspMiddle('\u00a0a')); + RawAssertions.assertEq('Should remain unchanged nbsp at end', 'a\u00a0', normalizeNbspMiddle('a\u00a0')); + RawAssertions.assertEq('Should remain unchanged 2 consecutive nbsps', 'a\u00a0\u00a0b', normalizeNbspMiddle('a\u00a0\u00a0b')); + RawAssertions.assertEq('Should remain unchanged nbsp followed by space', 'a\u00a0 b', normalizeNbspMiddle('a\u00a0 b')); + RawAssertions.assertEq('Should remain unchanged space followed by nbsp', 'a \u00a0b', normalizeNbspMiddle('a \u00a0b')); + RawAssertions.assertEq('Should remain unchanged space followed by space', 'a b', normalizeNbspMiddle('a b')); + + RawAssertions.assertEq('Should change middle nbsp to space', 'a b', normalizeNbspMiddle('a\u00a0b')); + RawAssertions.assertEq('Should change two nbsps to spaces', 'a b c', normalizeNbspMiddle('a\u00a0b\u00a0c')); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/BidiTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/BidiTest.ts new file mode 100644 index 000000000..62e72c1e8 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/BidiTest.ts @@ -0,0 +1,20 @@ +import { Assertions, Pipeline, Step } from '@ephox/agar'; +import * as Bidi from 'tinymce/core/text/Bidi'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('atomic.tinymce.core.text.BidiTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + + const sTestHasStrongRtl = Step.sync(function () { + Assertions.assertEq('Hebrew is strong rtl', true, Bidi.hasStrongRtl('\u05D4\u05E7\u05D3\u05E9')); + Assertions.assertEq('Abc is not strong rtl', false, Bidi.hasStrongRtl('abc')); + Assertions.assertEq('Dots are neutral', false, Bidi.hasStrongRtl('.')); + }); + + Pipeline.async({}, [ + sTestHasStrongRtl + ], function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/ExtendingCharTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/ExtendingCharTest.ts new file mode 100644 index 000000000..451d3fe90 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/ExtendingCharTest.ts @@ -0,0 +1,19 @@ +import { LegacyUnit } from '@ephox/mcagar'; +import { Pipeline } from '@ephox/agar'; +import * as ExtendingChar from 'tinymce/core/text/ExtendingChar'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('atomic.tinymce.core.text.ExtendingCharTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + suite.test('isExtendingChar', function () { + LegacyUnit.strictEqual(ExtendingChar.isExtendingChar('a'), false); + LegacyUnit.strictEqual(ExtendingChar.isExtendingChar('\u0301'), true); + }); + + Pipeline.async({}, suite.toSteps({}), function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/ZwspTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/ZwspTest.ts new file mode 100644 index 000000000..721c3edaf --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/atomic/text/ZwspTest.ts @@ -0,0 +1,26 @@ +import { LegacyUnit } from '@ephox/mcagar'; +import { Pipeline } from '@ephox/agar'; +import Zwsp from 'tinymce/core/text/Zwsp'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('atomic.tinymce.core.text.ZwspTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + suite.test('ZWSP', function () { + LegacyUnit.strictEqual(Zwsp.ZWSP, '\uFEFF'); + }); + + suite.test('isZwsp', function () { + LegacyUnit.strictEqual(Zwsp.isZwsp(Zwsp.ZWSP), true); + }); + + suite.test('isZwsp', function () { + LegacyUnit.strictEqual(Zwsp.trim('a' + Zwsp.ZWSP + 'b'), 'ab'); + }); + + Pipeline.async({}, suite.toSteps({}), function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/atomic/util/LazyEvaluatorTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/atomic/util/LazyEvaluatorTest.ts new file mode 100644 index 000000000..fe2a8ba5c --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/atomic/util/LazyEvaluatorTest.ts @@ -0,0 +1,34 @@ +import { Assertions, Pipeline, Step } from '@ephox/agar'; +import { Option } from '@ephox/katamari'; +import LazyEvaluator from 'tinymce/core/util/LazyEvaluator'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('atomic.tinymce.core.util.LazyEvaluatorTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + + const sTestEvaluateUntil = Step.sync(function () { + const operations = [ + function (a, b) { + return a === 1 && b === 'a' ? Option.some(1) : Option.none(); + }, + function (a, b) { + return a === 2 && b === 'b' ? Option.some(2) : Option.none(); + }, + function (a, b) { + return a === 3 && b === 'c' ? Option.some(3) : Option.none(); + } + ]; + + Assertions.assertEq('Should return none', true, LazyEvaluator.evaluateUntil(operations, [123, 'x']).isNone()); + Assertions.assertEq('Should return first item', 1, LazyEvaluator.evaluateUntil(operations, [1, 'a']).getOrDie(1)); + Assertions.assertEq('Should return second item', 2, LazyEvaluator.evaluateUntil(operations, [2, 'b']).getOrDie(2)); + Assertions.assertEq('Should return third item', 3, LazyEvaluator.evaluateUntil(operations, [3, 'c']).getOrDie(3)); + }); + + Pipeline.async({}, [ + sTestEvaluateUntil + ], function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/AddOnManagerTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/AddOnManagerTest.ts new file mode 100644 index 000000000..eaebe1dc4 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/AddOnManagerTest.ts @@ -0,0 +1,88 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit } from '@ephox/mcagar'; +import { AddOnManager } from 'tinymce/core/api/AddOnManager'; +import ScriptLoader from 'tinymce/core/api/dom/ScriptLoader'; +import PluginManager from 'tinymce/core/api/PluginManager'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.AddOnManagerTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + let languagePackUrl; + + const patch = function (proto, name, patchFunc) { + let originalFunc = proto[name]; + let originalFuncs = proto.__originalFuncs; + + if (!originalFuncs) { + proto.__originalFuncs = originalFuncs = {}; + } + + if (!originalFuncs[name]) { + originalFuncs[name] = originalFunc; + } else { + originalFunc = originalFuncs[name]; + } + + proto[name] = function () { + const args = Array.prototype.slice.call(arguments); + args.unshift(originalFunc); + return patchFunc.apply(this, args); + }; + }; + + const unpatch = function (proto, name?) { + const originalFuncs = proto.__originalFuncs; + + if (!originalFuncs) { + return; + } + + if (name) { + proto[name] = originalFuncs[name]; + delete originalFuncs[name]; + } else { + for (const key in originalFuncs) { + proto[key] = originalFuncs[key]; + } + + delete proto.__originalFuncs; + } + }; + + const getLanguagePackUrl = function (language, languages?) { + languagePackUrl = null; + AddOnManager.language = language; + PluginManager.requireLangPack('plugin', languages); + return languagePackUrl; + }; + + suite.test('requireLangPack', function () { + AddOnManager.PluginManager.urls.plugin = '/root'; + + LegacyUnit.equal(getLanguagePackUrl('sv_SE'), '/root/langs/sv_SE.js'); + LegacyUnit.equal(getLanguagePackUrl('sv_SE', 'sv,en,us'), '/root/langs/sv.js'); + LegacyUnit.equal(getLanguagePackUrl('sv_SE', 'sv_SE,en_US'), '/root/langs/sv_SE.js'); + LegacyUnit.equal(getLanguagePackUrl('sv'), '/root/langs/sv.js'); + LegacyUnit.equal(getLanguagePackUrl('sv', 'sv'), '/root/langs/sv.js'); + LegacyUnit.equal(getLanguagePackUrl('sv', 'sv,en,us'), '/root/langs/sv.js'); + LegacyUnit.equal(getLanguagePackUrl('sv', 'en,sv,us'), '/root/langs/sv.js'); + LegacyUnit.equal(getLanguagePackUrl('sv', 'en,us,sv'), '/root/langs/sv.js'); + LegacyUnit.strictEqual(getLanguagePackUrl('sv', 'en,us'), null); + LegacyUnit.strictEqual(getLanguagePackUrl(null, 'en,us'), null); + LegacyUnit.strictEqual(getLanguagePackUrl(null), null); + + AddOnManager.languageLoad = false; + LegacyUnit.strictEqual(getLanguagePackUrl('sv', 'sv'), null); + }); + + patch(ScriptLoader.ScriptLoader, 'add', function (origFunc, url) { + languagePackUrl = url; + }); + + Pipeline.async({}, suite.toSteps({}), function () { + success(); + unpatch(ScriptLoader.ScriptLoader); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/ClickContentEditableFalseTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/ClickContentEditableFalseTest.ts new file mode 100644 index 000000000..6f2f13fdb --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/ClickContentEditableFalseTest.ts @@ -0,0 +1,66 @@ +import { GeneralSteps, Logger, Pipeline, Step } from '@ephox/agar'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import { Hierarchy, Element } from '@ephox/sugar'; +import TypeText from '../module/test/TypeText'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.ClickContentEditableFalseTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + + Theme(); + + const sClickMiddleOf = function (editor, elementPath) { + return Step.sync(function () { + const element = Hierarchy.follow(Element.fromDom(editor.getBody()), elementPath).getOrDie().dom(); + const rect = element.getBoundingClientRect(); + const clientX = (rect.left + rect.width / 2), clientY = (rect.top + rect.height / 2); + + editor.fire('mousedown', { target: element, clientX, clientY }); + editor.fire('mouseup', { target: element, clientX, clientY }); + editor.fire('click', { target: element, clientX, clientY }); + }); + }; + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + const tinyApis = TinyApis(editor); + + Pipeline.async({}, [ + tinyApis.sFocus, + Logger.t('Click on content editable false', GeneralSteps.sequence([ + tinyApis.sSetContent('a
'), + sClickMiddleOf(editor, [1]), + tinyApis.sAssertSelection([], 0, [], 1) + ])), + Logger.t('Click on content editable false inside content editable true', GeneralSteps.sequence([ + tinyApis.sSetContent('a
a
a
b
a
bc
a
a
'), + sClickMiddleOf(editor, [1]), + sClickMiddleOf(editor, [1]), + tinyApis.sAssertSelection([1, 0], 1, [1, 0], 1) + ])) + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + indent: false + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/DragDropOverridesTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/DragDropOverridesTest.ts new file mode 100644 index 000000000..1b618a931 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/DragDropOverridesTest.ts @@ -0,0 +1,40 @@ +import { Assertions, GeneralSteps, Logger, Pipeline, Step } from '@ephox/agar'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import { Cell } from '@ephox/katamari'; +import { Hierarchy, Element } from '@ephox/sugar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.DragDropOverridesTest', (success, failure) => { + Theme(); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + const tinyApis = TinyApis(editor); + const fired = Cell(false); + + editor.on('dragend', function () { + fired.set(true); + }); + + Pipeline.async({}, [ + Logger.t('drop draggable element outside of editor', GeneralSteps.sequence([ + tinyApis.sSetContent('a
'), + Step.sync(() => { + const target = Hierarchy.follow(Element.fromDom(editor.getBody()), [0]).getOrDie().dom(); + const rect = target.getBoundingClientRect(); + const button = 0, screenX = (rect.left + rect.width / 2), screenY = (rect.top + rect.height / 2); + + editor.fire('mousedown', { button, screenX, screenY, target }); + editor.fire('mousemove', { button, screenX: screenX + 20, screenY: screenY + 20, target }); + editor.dom.fire(document.body, 'mouseup'); + + Assertions.assertEq('Should fire dragend event', true, fired.get()); + }) + ])), + ], onSuccess, onFailure); + }, { + indent: false, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorForcedSettingsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorForcedSettingsTest.ts new file mode 100644 index 000000000..1b177e093 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorForcedSettingsTest.ts @@ -0,0 +1,26 @@ +import { Assertions, Pipeline } from '@ephox/agar'; +import { TinyLoader } from '@ephox/mcagar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.EditorForcedSettingsTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + + Theme(); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, [ + Assertions.sAssertEq('Validate should always be true', true, editor.settings.validate), + Assertions.sAssertEq('Validate should true since inline was set to true', true, editor.settings.content_editable) + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + + // Setting exposed as another forced setting + inline: true, + + // Settings that are to be forced + validate: false + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorManagerTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorManagerTest.ts new file mode 100644 index 000000000..e033d5d5d --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorManagerTest.ts @@ -0,0 +1,215 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit } from '@ephox/mcagar'; +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import { Editor } from 'tinymce/core/api/Editor'; +import EditorManager from 'tinymce/core/api/EditorManager'; +import PluginManager from 'tinymce/core/api/PluginManager'; +import ViewBlock from '../module/test/ViewBlock'; +import Delay from 'tinymce/core/api/util/Delay'; +import Tools from 'tinymce/core/api/util/Tools'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.EditorManagerTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + const viewBlock = ViewBlock(); + + Theme(); + + const teardown = function (done) { + Delay.setTimeout(function () { + EditorManager.remove(); + done(); + }, 0); + }; + + suite.asyncTest('get', function (_, done) { + viewBlock.update(''); + EditorManager.init({ + selector: 'textarea.tinymce', + skin_url: '/project/js/tinymce/skins/lightgray', + init_instance_callback (editor1) { + LegacyUnit.equal(EditorManager.get().length, 1); + LegacyUnit.equal(EditorManager.get(0) === EditorManager.activeEditor, true); + LegacyUnit.equal(EditorManager.get(1), null); + LegacyUnit.equal(EditorManager.get('noid'), null); + LegacyUnit.equal(EditorManager.get(undefined), null); + LegacyUnit.equal(EditorManager.get()[0] === EditorManager.activeEditor, true); + LegacyUnit.equal(EditorManager.get(EditorManager.activeEditor.id) === EditorManager.activeEditor, true); + LegacyUnit.equal(EditorManager.get() !== EditorManager.get(), true); + + // Trigger save + let saveCount = 0; + + editor1.on('SaveContent', function () { + saveCount++; + }); + + EditorManager.triggerSave(); + LegacyUnit.equal(saveCount, 1); + + // Re-init on same id + EditorManager.init({ + selector: '#' + EditorManager.activeEditor.id, + skin_url: '/project/js/tinymce/skins/lightgray' + }); + + LegacyUnit.equal(EditorManager.get().length, 1); + + teardown(done); + } + }); + }); + + suite.test('addI18n/translate', function () { + EditorManager.addI18n('en', { + from: 'to' + }); + + LegacyUnit.equal(EditorManager.translate('from'), 'to'); + }); + + suite.asyncTest('Do not reload language pack if it was already loaded or registered manually.', function (_, done) { + const langCode = 'mce_lang'; + const langUrl = 'http://example.com/language/' + langCode + '.js'; + + EditorManager.addI18n(langCode, { + from: 'to' + }); + + viewBlock.update(''); + + EditorManager.init({ + selector: 'textarea', + skin_url: '/project/js/tinymce/skins/lightgray', + language: langCode, + language_url: langUrl, + init_instance_callback (ed) { + const scripts = Tools.grep(document.getElementsByTagName('script'), function (script) { + return script.src === langUrl; + }); + + LegacyUnit.equal(scripts.length, 0); + + teardown(done); + } + }); + }); + + suite.asyncTest('Externally destroyed editor', function (_, done) { + EditorManager.remove(); + + EditorManager.init({ + selector: 'textarea', + skin_url: '/project/js/tinymce/skins/lightgray', + init_instance_callback (editor1) { + Delay.setTimeout(function () { + // Destroy the editor by setting innerHTML common ajax pattern + viewBlock.update(''); + + // Re-init the editor will have the same id + EditorManager.init({ + selector: 'textarea', + skin_url: '/project/js/tinymce/skins/lightgray', + init_instance_callback (editor2) { + LegacyUnit.equal(EditorManager.get().length, 1); + LegacyUnit.equal(editor1.id, editor2.id); + LegacyUnit.equal(editor1.destroyed, true, 'First editor instance should be destroyed'); + + teardown(done); + } + }); + }, 0); + } + }); + }); + + suite.test('overrideDefaults', function () { + let oldBaseURI, oldBaseUrl, oldSuffix; + + oldBaseURI = EditorManager.baseURI; + oldBaseUrl = EditorManager.baseURL; + oldSuffix = EditorManager.suffix; + + EditorManager.overrideDefaults({ + test: 42, + base_url: 'http://www.EditorManager.com/base/', + suffix: 'x', + external_plugins: { + plugina: '//domain/plugina.js', + pluginb: '//domain/pluginb.js' + }, + plugin_base_urls: { + testplugin: 'http://custom.ephox.com/dir/testplugin' + } + }); + + LegacyUnit.strictEqual(EditorManager.baseURI.path, '/base'); + LegacyUnit.strictEqual(EditorManager.baseURL, 'http://www.EditorManager.com/base'); + LegacyUnit.strictEqual(EditorManager.suffix, 'x'); + LegacyUnit.strictEqual(new Editor('ed1', {}, EditorManager).settings.test, 42); + LegacyUnit.strictEqual(PluginManager.urls.testplugin, 'http://custom.ephox.com/dir/testplugin'); + + LegacyUnit.equal(new Editor('ed2', { + skin_url: '/project/js/tinymce/skins/lightgray', + external_plugins: { + plugina: '//domain/plugina2.js', + pluginc: '//domain/pluginc.js' + }, + plugin_base_urls: { + testplugin: 'http://custom.ephox.com/dir/testplugin' + } + }, EditorManager).settings.external_plugins, { + plugina: '//domain/plugina2.js', + pluginb: '//domain/pluginb.js', + pluginc: '//domain/pluginc.js' + }); + + LegacyUnit.equal(new Editor('ed3', { + skin_url: '/project/js/tinymce/skins/lightgray' + }, EditorManager).settings.external_plugins, { + plugina: '//domain/plugina.js', + pluginb: '//domain/pluginb.js' + }); + + EditorManager.baseURI = oldBaseURI; + EditorManager.baseURL = oldBaseUrl; + EditorManager.suffix = oldSuffix; + + EditorManager.overrideDefaults({}); + }); + + suite.test('Init inline editor on invalid targets', function () { + let invalidNames; + + invalidNames = ( + 'area base basefont br col frame hr img input isindex link meta param embed source wbr track ' + + 'colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu' + ); + + EditorManager.remove(); + + Tools.each(invalidNames.split(' '), function (invalidName) { + const elm = DOMUtils.DOM.add(document.body, invalidName, { class: 'targetEditor' }, null); + + EditorManager.init({ + selector: invalidName + '.targetEditor', + skin_url: '/project/js/tinymce/skins/lightgray', + inline: true + }); + + LegacyUnit.strictEqual(EditorManager.get().length, 0, 'Should not have created an editor'); + DOMUtils.DOM.remove(elm); + }); + }); + + viewBlock.attach(); + Pipeline.async({}, suite.toSteps({}), function () { + EditorManager.remove(); + viewBlock.detach(); + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRemoveTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRemoveTest.ts new file mode 100644 index 000000000..1414a9462 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRemoveTest.ts @@ -0,0 +1,74 @@ +import { Chain, Logger, Pipeline, RawAssertions } from '@ephox/agar'; +import { UnitTest } from '@ephox/bedrock'; +import { Editor as McEditor } from '@ephox/mcagar'; + +import { Editor } from 'tinymce/core/api/Editor'; +import EditorManager from 'tinymce/core/api/EditorManager'; +import Theme from 'tinymce/themes/modern/Theme'; + +UnitTest.asynctest('browser.tinymce.core.EditorRemoveTest', (success, failure) => { + Theme(); + + const settings = { + skin_url: '/project/js/tinymce/skins/lightgray' + }; + + const cAssertTextareaDisplayStyle = (expected) => Chain.op((editor: any) => { + const textareaElement = editor.getElement(); + + RawAssertions.assertEq('element does not have the expected style', expected, textareaElement.style.display); + }); + + const cCreateEditor = Chain.mapper(() => new Editor('editor', {}, EditorManager)); + + const cRemoveEditor = Chain.op((editor: any) => editor.remove()); + + Pipeline.async({}, [ + Logger.t('remove editor without initializing it', Chain.asStep({}, [ + cCreateEditor, + cRemoveEditor, + ])), + + Logger.t('remove editor where the body has been removed', Chain.asStep({}, [ + McEditor.cFromHtml('', settings), + Chain.mapper((value) => { + const body = value.getBody(); + body.parentNode.removeChild(body); + return value; + }), + McEditor.cRemove + ])), + + Logger.t('init editor with no display style', Chain.asStep({}, [ + McEditor.cFromHtml('', settings), + cAssertTextareaDisplayStyle('none'), + cRemoveEditor, + cAssertTextareaDisplayStyle(''), + Chain.op((editor) => EditorManager.init({ selector: '#tinymce' })), + cAssertTextareaDisplayStyle(''), + McEditor.cRemove + ])), + + Logger.t('init editor with display: none', Chain.asStep({}, [ + McEditor.cFromHtml('', settings), + cAssertTextareaDisplayStyle('none'), + cRemoveEditor, + cAssertTextareaDisplayStyle('none'), + Chain.op((editor) => EditorManager.init({ selector: '#tinymce' })), + cAssertTextareaDisplayStyle('none'), + McEditor.cRemove + ])), + + Logger.t('init editor with display: block', Chain.asStep({}, [ + McEditor.cFromHtml('', settings), + cAssertTextareaDisplayStyle('none'), + cRemoveEditor, + cAssertTextareaDisplayStyle('block'), + Chain.op((editor) => EditorManager.init({ selector: '#tinymce' })), + cAssertTextareaDisplayStyle('block'), + McEditor.cRemove + ])) + ], () => { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRemovedApiTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRemovedApiTest.ts new file mode 100644 index 000000000..fa3ffe953 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRemovedApiTest.ts @@ -0,0 +1,112 @@ +import { Assertions, GeneralSteps, Logger, Pipeline, Step } from '@ephox/agar'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.EditorApiTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + + Theme(); + + const sRemoveEditor = function (editor) { + return Step.sync(function () { + editor.remove(); + }); + }; + + const sExecCallback = function (editor, name, arg) { + return Step.sync(function () { + editor.execCallback(name, arg); + }); + }; + + const sTryAccess = function (editor, name, expectedValue) { + return Step.sync(function () { + const result = editor[name](); + Assertions.assertEq('Should be expected value on a removed editor', expectedValue, result); + }); + }; + + const sShow = function (editor) { + return Step.sync(function () { + editor.show(); + }); + }; + + const sHide = function (editor) { + return Step.sync(function () { + editor.hide(); + }); + }; + + const sLoad = function (editor) { + return Step.sync(function () { + editor.load(); + }); + }; + + const sSave = function (editor) { + return Step.sync(function () { + editor.save(); + }); + }; + + const sQueryCommandState = function (editor, name) { + return Step.sync(function () { + editor.queryCommandState(name); + }); + }; + + const sQueryCommandValue = function (editor, name) { + return Step.sync(function () { + editor.queryCommandValue(name); + }); + }; + + const sQueryCommandSupported = function (editor, name) { + return Step.sync(function () { + editor.queryCommandSupported(name); + }); + }; + + const sUploadImages = function (editor) { + return Step.sync(function () { + editor.uploadImages(function () { + }); + }); + }; + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + const tinyApis = TinyApis(editor); + + Pipeline.async({}, [ + sRemoveEditor(editor), + Logger.t('Try to access/execute things on an editor that does not exists', GeneralSteps.sequence([ + sTryAccess(editor, 'getBody', null), + sTryAccess(editor, 'getDoc', null), + sTryAccess(editor, 'getWin', null), + sTryAccess(editor, 'getContent', ''), + sTryAccess(editor, 'getContainer', null), + sTryAccess(editor, 'getContentAreaContainer', null), + sLoad(editor), + sSave(editor), + sShow(editor), + sHide(editor), + sQueryCommandState(editor, 'bold'), + sQueryCommandValue(editor, 'bold'), + sQueryCommandSupported(editor, 'bold'), + sUploadImages(editor), + tinyApis.sSetContent('a'), + tinyApis.sExecCommand('bold'), + tinyApis.sFocus, + tinyApis.sNodeChanged, + sExecCallback(editor, 'test_callback', 1) + ])) + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + test_callback () { + } + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRtlTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRtlTest.ts new file mode 100644 index 000000000..edf0402ae --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorRtlTest.ts @@ -0,0 +1,54 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import ScriptLoader from 'tinymce/core/api/dom/ScriptLoader'; +import EditorManager from 'tinymce/core/api/EditorManager'; +import Factory from 'tinymce/core/api/ui/Factory'; +import I18n from 'tinymce/core/api/util/I18n'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.EditorRtlTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + const teardown = function () { + I18n.rtl = false; + I18n.setCode('en'); + Factory.get('Control').rtl = false; + }; + + suite.test('UI rendered in RTL mode', function () { + LegacyUnit.equal(EditorManager.activeEditor.getContainer().className.indexOf('mce-rtl') !== -1, true, 'Should have a mce-rtl class'); + LegacyUnit.equal(EditorManager.activeEditor.rtl, true, 'Should have the rtl property set'); + }); + + EditorManager.addI18n('ar', { + Bold: 'Bold test', + _dir: 'rtl' + }); + + // Prevents the arabic language pack from being loaded + EditorManager.overrideDefaults({ + base_url: '/project/tinymce' + }); + ScriptLoader.ScriptLoader.markDone('http://' + document.location.host + '/project/tinymce/langs/ar.js'); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), function () { + teardown(); + onSuccess(); + }, onFailure); + }, { + language: 'ar', + selector: 'textarea', + add_unload_trigger: false, + disable_nodechange: true, + entities: 'raw', + indent: false, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorSettingsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorSettingsTest.ts new file mode 100644 index 000000000..c094cda10 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorSettingsTest.ts @@ -0,0 +1,295 @@ +import { Assertions, GeneralSteps, Logger, Pipeline, Step } from '@ephox/agar'; +import { TinyLoader } from '@ephox/mcagar'; +import { PlatformDetection } from '@ephox/sand'; +import * as EditorSettings from 'tinymce/core/EditorSettings'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { Editor } from 'tinymce/core/api/Editor'; +import EditorManager from 'tinymce/core/api/EditorManager'; + +UnitTest.asynctest('browser.tinymce.core.EditorSettingsTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const detection = PlatformDetection.detect(); + const isTouch = detection.deviceType.isTouch(); + + Theme(); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, [ + Logger.t('getEditorSettings tests', GeneralSteps.sequence([ + Logger.t('Override defaults plugins', Step.sync(function () { + const settings = EditorSettings.getEditorSettings( + editor, + 'id', + 'documentBaseUrl', + { + defaultSetting: 'a', + plugins: ['a'] + }, + { + validate: false, + userSetting: 'b' + } + ); + + Assertions.assertEq('Should have the specified id', 'id', settings.id); + Assertions.assertEq('Should have the specified documentBaseUrl', 'documentBaseUrl', settings.document_base_url); + Assertions.assertEq('Should have the specified userSetting', 'b', settings.userSetting); + Assertions.assertEq('Should have the forced validate setting', true, settings.validate); + Assertions.assertEq('Should have the default theme', 'modern', settings.theme); + Assertions.assertEq('Should have the specified default plugin', 'a', settings.plugins); + Assertions.assertEq('Should have the default setting', 'a', settings.defaultSetting); + })), + + Logger.t('Override defaults with forced_plugins using arrays', Step.sync(function () { + const defaultSettings = { + forced_plugins: ['a', 'b'] + }; + + const userSettings = { + plugins: ['c', 'd'] + }; + + const settings = EditorSettings.getEditorSettings(editor, 'id', 'documentBaseUrl', defaultSettings, userSettings); + + Assertions.assertEq('Should be both forced and user plugins', 'a b c d', settings.plugins); + })), + + Logger.t('Override defaults with forced_plugins using strings', Step.sync(function () { + const defaultSettings = { + forced_plugins: 'a b' + }; + + const userSettings = { + plugins: 'c d' + }; + + const settings = EditorSettings.getEditorSettings(editor, 'id', 'documentBaseUrl', defaultSettings, userSettings); + + Assertions.assertEq('Should be both forced and user plugins', 'a b c d', settings.plugins); + })), + + Logger.t('Override defaults with forced_plugins using mixed types and spaces', Step.sync(function () { + const defaultSettings = { + forced_plugins: ' a b' + }; + + const userSettings = { + plugins: [' c ', ' d e '] + }; + + const settings = EditorSettings.getEditorSettings(editor, 'id', 'documentBaseUrl', defaultSettings, userSettings); + + Assertions.assertEq('Should be both forced and user plugins', 'a b c d e', settings.plugins); + })), + + Logger.t('Override defaults with just default forced_plugins', Step.sync(function () { + const defaultSettings = { + forced_plugins: ['a', 'b'] + }; + + const userSettings = { + }; + + const settings = EditorSettings.getEditorSettings(editor, 'id', 'documentBaseUrl', defaultSettings, userSettings); + + Assertions.assertEq('Should be just default plugins', 'a b', settings.plugins); + })), + + Logger.t('Override defaults with just user plugins', Step.sync(function () { + const defaultSettings = { + }; + + const userSettings = { + plugins: ['a', 'b'] + }; + + const settings = EditorSettings.getEditorSettings(editor, 'id', 'documentBaseUrl', defaultSettings, userSettings); + + Assertions.assertEq('Should be just user plugins', 'a b', settings.plugins); + })), + + Logger.t('Override defaults with forced_plugins should not be possible to override', Step.sync(function () { + const defaultSettings = { + forced_plugins: ['a', 'b'] + }; + + const userSettings = { + forced_plugins: ['a'], + plugins: ['c', 'd'] + }; + + const settings = EditorSettings.getEditorSettings(editor, 'id', 'documentBaseUrl', defaultSettings, userSettings); + + Assertions.assertEq('Should be just forced and user plugins', 'a b c d', settings.plugins); + })), + + Logger.t('Getters for varous setting types', Step.sync(function () { + const settings = EditorSettings.getEditorSettings( + {}, + 'id', + 'documentBaseUrl', + { + plugins: ['a'] + }, + { + string: 'a', + number: 1, + boolTrue: true, + boolFalse: false, + null: null, + undef: undefined + } + ); + + const fakeEditor = { + settings + }; + + Assertions.assertEq('Should be none for non existing setting', true, EditorSettings.get(fakeEditor, 'non_existing').isNone()); + Assertions.assertEq('Should be none for existing null setting', true, EditorSettings.get(fakeEditor, 'non_existing').isNone()); + Assertions.assertEq('Should be none for existing undefined setting', true, EditorSettings.get(fakeEditor, 'undef').isNone()); + Assertions.assertEq('Should be some for existing string setting', 'a', EditorSettings.get(fakeEditor, 'string').getOrDie()); + Assertions.assertEq('Should be some for existing number setting', 1, EditorSettings.get(fakeEditor, 'number').getOrDie()); + Assertions.assertEq('Should be some for existing bool setting', true, EditorSettings.get(fakeEditor, 'boolTrue').getOrDie()); + Assertions.assertEq('Should be some for existing bool setting', false, EditorSettings.get(fakeEditor, 'boolFalse').getOrDie()); + Assertions.assertEq('Should be none for non existing setting', true, EditorSettings.getString(fakeEditor, 'non_existing').isNone()); + Assertions.assertEq('Should be some for existing string setting', 'a', EditorSettings.getString(fakeEditor, 'string').getOrDie()); + Assertions.assertEq('Should be none for existing number setting', true, EditorSettings.getString(fakeEditor, 'number').isNone()); + Assertions.assertEq('Should be none for existing bool setting', true, EditorSettings.getString(fakeEditor, 'boolTrue').isNone()); + })), + + Logger.t('Mobile override', Step.sync(function () { + const settings = EditorSettings.getEditorSettings( + {}, + 'id', + 'documentBaseUrl', + { + settingB: false + }, + { + mobile: { + settingA: true, + settingB: true + } + } + ); + + const fakeEditor = { + settings + }; + + Assertions.assertEq('Should only have the mobile setting on touch', EditorSettings.get(fakeEditor, 'settingA').getOr(false), isTouch); + Assertions.assertEq('Should not have a mobile setting on desktop', EditorSettings.get(fakeEditor, 'settingA').isNone(), !isTouch); + Assertions.assertEq('Should have the expected mobile setting value on touch', EditorSettings.get(fakeEditor, 'settingB').getOr(false), isTouch); + Assertions.assertEq('Should have the expected desktop setting on desktop', EditorSettings.get(fakeEditor, 'settingB').getOr(true), isTouch); + })) + ])), + + Logger.t('combineSettings tests', GeneralSteps.sequence([ + Logger.t('Merged settings (desktop)', Step.sync(function () { + Assertions.assertEq( + 'Should be have validate forced and empty plugins the merged settings', + { a: 1, b: 2, c: 3, validate: true, external_plugins: {}, plugins: '' }, + EditorSettings.combineSettings(false, { a: 1, b: 1, c: 1 }, { b: 2 }, { c: 3 }) + ); + })), + + Logger.t('Merged settings forced_plugins in default override settings (desktop)', Step.sync(function () { + Assertions.assertEq( + 'Should be have plugins merged with forced plugins', + { validate: true, external_plugins: {}, forced_plugins: ['a'], plugins: 'a b' }, + EditorSettings.combineSettings(false, {}, { forced_plugins: ['a'] }, { plugins: ['b'] }) + ); + })), + + Logger.t('Merged settings (mobile)', Step.sync(function () { + Assertions.assertEq( + 'Should be have validate forced and empty plugins the merged settings', + { a: 1, b: 2, c: 3, validate: true, external_plugins: {}, plugins: '' }, + EditorSettings.combineSettings(true, { a: 1, b: 1, c: 1 }, { b: 2 }, { c: 3 }) + ); + })), + + Logger.t('Merged settings forced_plugins in default override settings (mobile)', Step.sync(function () { + Assertions.assertEq( + 'Should be have plugins merged with forced plugins', + { validate: true, external_plugins: {}, forced_plugins: ['a'], plugins: 'a b' }, + EditorSettings.combineSettings(true, {}, { forced_plugins: ['a'] }, { plugins: ['b'] }) + ); + })), + + Logger.t('Merged settings forced_plugins in default override settings with user mobile settings (desktop)', Step.sync(function () { + Assertions.assertEq( + 'Should not have plugins merged with mobile plugins', + { validate: true, external_plugins: {}, forced_plugins: ['a'], plugins: 'a b' }, + EditorSettings.combineSettings(false, {}, { forced_plugins: ['a'] }, { plugins: ['b'], mobile: { plugins: ['c'] } }) + ); + })), + + Logger.t('Merged settings forced_plugins in default override settings with user mobile settings (mobile)', Step.sync(function () { + Assertions.assertEq( + 'Should have forced_plugins merged with mobile plugins but only whitelisted user plugins', + { validate: true, external_plugins: {}, forced_plugins: ['a'], plugins: 'a lists', theme: 'mobile' }, + EditorSettings.combineSettings(true, {}, { forced_plugins: ['a'] }, { plugins: ['b'], mobile: { plugins: ['lists custom'] } }) + ); + })), + + Logger.t('Merged settings forced_plugins in default override forced_plugins in user settings', Step.sync(function () { + Assertions.assertEq( + 'Should not have user forced plugins', + { validate: true, external_plugins: {}, forced_plugins: ['b'], plugins: 'a' }, + EditorSettings.combineSettings(false, {}, { forced_plugins: ['a'] }, { forced_plugins: ['b'] }) + ); + })) + ])), + Logger.t('getParam hash (legacy)', Step.sync(function () { + const editor = new Editor('id', { + hash1: 'a,b,c', + hash2: 'a', + hash3: 'a=b', + hash4: 'a=b;c=d,e', + hash5: 'a=b,c=d' + }, EditorManager); + + Assertions.assertEq('Should be expected object', { a: 'a', b: 'b', c: 'c' }, EditorSettings.getParam(editor, 'hash1', {}, 'hash')); + Assertions.assertEq('Should be expected object', { a: 'a' }, EditorSettings.getParam(editor, 'hash2', {}, 'hash')); + Assertions.assertEq('Should be expected object', { a: 'b' }, EditorSettings.getParam(editor, 'hash3', {}, 'hash')); + Assertions.assertEq('Should be expected object', { a: 'b', c: 'd,e' }, EditorSettings.getParam(editor, 'hash4', {}, 'hash')); + Assertions.assertEq('Should be expected object', { a: 'b', c: 'd' }, EditorSettings.getParam(editor, 'hash5', {}, 'hash')); + Assertions.assertEq('Should be expected default object', { b: 2 }, EditorSettings.getParam(editor, 'hash_undefined', { b: 2 }, 'hash')); + })), + Logger.t('getParam primary types', Step.sync(function () { + const editor = new Editor('id', { + bool: true, + str: 'a', + num: 2, + obj: { a: 1 }, + arr: [ 'a' ], + fun: () => {}, + strArr: ['a', 'b'], + mixedArr: ['a', 3] + }, EditorManager); + + Assertions.assertEq('Should be expected bool', true, EditorSettings.getParam(editor, 'bool', false, 'boolean')); + Assertions.assertEq('Should be expected string', 'a', EditorSettings.getParam(editor, 'str', 'x', 'string')); + Assertions.assertEq('Should be expected number', 2, EditorSettings.getParam(editor, 'num', 1, 'number')); + Assertions.assertEq('Should be expected object', { a: 1 }, EditorSettings.getParam(editor, 'obj', {}, 'object')); + Assertions.assertEq('Should be expected array', [ 'a' ], EditorSettings.getParam(editor, 'arr', [], 'array')); + Assertions.assertEq('Should be expected function', 'function', typeof EditorSettings.getParam(editor, 'fun', null, 'function')); + Assertions.assertEq('Should be expected default bool', false, EditorSettings.getParam(editor, 'bool_undefined', false, 'boolean')); + Assertions.assertEq('Should be expected default string', 'x', EditorSettings.getParam(editor, 'str_undefined', 'x', 'string')); + Assertions.assertEq('Should be expected default number', 1, EditorSettings.getParam(editor, 'num_undefined', 1, 'number')); + Assertions.assertEq('Should be expected default object', {}, EditorSettings.getParam(editor, 'obj_undefined', {}, 'object')); + Assertions.assertEq('Should be expected default array', [], EditorSettings.getParam(editor, 'arr_undefined', [], 'array')); + Assertions.assertEq('Should be expected default function', null, EditorSettings.getParam(editor, 'fun_undefined', null, 'function')); + Assertions.assertEq('Should be expected string array', ['a', 'b'], EditorSettings.getParam(editor, 'strArr', ['x'], 'string[]')); + Assertions.assertEq('Should be expected default array on mixed types', ['x'], EditorSettings.getParam(editor, 'mixedArr', ['x'], 'string[]')); + Assertions.assertEq('Should be expected default array on boolean', ['x'], EditorSettings.getParam(editor, 'bool', ['x'], 'string[]')); + })) + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorTest.ts new file mode 100644 index 000000000..7e35f02cd --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorTest.ts @@ -0,0 +1,458 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import EditorManager from 'tinymce/core/api/EditorManager'; +import Env from 'tinymce/core/api/Env'; +import HtmlUtils from '../module/test/HtmlUtils'; +import URI from 'tinymce/core/api/util/URI'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.EditorTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + suite.test('Event: change', function (editor) { + let level, lastLevel; + + editor.on('change', function (e) { + level = e.level; + lastLevel = e.lastLevel; + }); + + editor.setContent(''); + editor.insertContent('a'); + LegacyUnit.equal(level.content.toLowerCase(), 'a
'); + LegacyUnit.equal(lastLevel.content, editor.undoManager.data[0].content); + + editor.off('change'); + }); + + suite.test('Event: beforeExecCommand', function (editor) { + let cmd, ui, value; + + editor.on('BeforeExecCommand', function (e) { + cmd = e.command; + ui = e.ui; + value = e.value; + + e.preventDefault(); + }); + + editor.setContent(''); + editor.insertContent('a'); + LegacyUnit.equal(editor.getContent(), ''); + LegacyUnit.equal(cmd, 'mceInsertContent'); + LegacyUnit.equal(ui, false); + LegacyUnit.equal(value, 'a'); + + editor.off('BeforeExecCommand'); + editor.setContent(''); + editor.insertContent('a'); + LegacyUnit.equal(editor.getContent(), 'a
'); + }); + + suite.test('urls - relativeURLs', function (editor) { + editor.settings.relative_urls = true; + editor.documentBaseURI = new URI('http://www.site.com/dirA/dirB/dirC/'); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('urls - absoluteURLs', function (editor) { + editor.settings.relative_urls = false; + editor.settings.remove_script_host = true; + editor.documentBaseURI = new URI('http://www.site.com/dirA/dirB/dirC/'); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.settings.relative_urls = false; + editor.settings.remove_script_host = false; + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + + editor.setContent('test'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('WebKit Serialization range bug', function (editor) { + if (Env.webkit) { + // Note that if we create the P with this invalid content directly, Chrome cleans it up differently to other browsers so we don't + // wind up testing the serialization functionality we were aiming for and the test fails. + const p = editor.dom.create('p', {}, '123| X |
123
| X |
456
'); + } + }); + + suite.test('editor_methods - getParam', function (editor) { + editor.settings.test = 'a,b,c'; + LegacyUnit.equal(editor.getParam('test', '', 'hash').c, 'c'); + + editor.settings.test = 'a'; + LegacyUnit.equal(editor.getParam('test', '', 'hash').a, 'a'); + + editor.settings.test = 'a=b'; + LegacyUnit.equal(editor.getParam('test', '', 'hash').a, 'b'); + + editor.settings.test = 'a=b;c=d,e'; + LegacyUnit.equal(editor.getParam('test', '', 'hash').c, 'd,e'); + + editor.settings.test = 'a=b,c=d'; + LegacyUnit.equal(editor.getParam('test', '', 'hash').c, 'd'); + }); + + suite.test('setContent', function (editor) { + let count; + + const callback = function (e) { + e.content = e.content.replace(/test/, 'X'); + count++; + }; + + editor.on('SetContent', callback); + editor.on('BeforeSetContent', callback); + count = 0; + editor.setContent('test
'); + LegacyUnit.equal(editor.getContent(), 'X
'); + LegacyUnit.equal(count, 2); + editor.off('SetContent', callback); + editor.off('BeforeSetContent', callback); + + count = 0; + editor.setContent('test
'); + LegacyUnit.equal(editor.getContent(), 'test
'); + LegacyUnit.equal(count, 0); + }); + + suite.test('setContent with comment bug #4409', function (editor) { + editor.setContent('\u00a0
'); + }); + + suite.test('custom elements', function (editor) { + editor.setContent('abc
'); + LegacyUnit.equal(editor.getContent(), 'abc
'); + }); + + suite.test('show/hide/isHidden and events', function (editor) { + let lastEvent; + + editor.on('show hide', function (e) { + lastEvent = e; + }); + + LegacyUnit.equal(editor.isHidden(), false, 'Initial isHidden state'); + + editor.hide(); + LegacyUnit.equal(editor.isHidden(), true, 'After hide isHidden state'); + LegacyUnit.equal(lastEvent.type, 'hide'); + + lastEvent = null; + editor.hide(); + LegacyUnit.equal(lastEvent, null); + + editor.show(); + LegacyUnit.equal(editor.isHidden(), false, 'After show isHidden state'); + LegacyUnit.equal(lastEvent.type, 'show'); + + lastEvent = null; + editor.show(); + LegacyUnit.equal(lastEvent, null); + }); + + suite.test('hide save content and hidden state while saving', function (editor) { + let lastEvent, hiddenStateWhileSaving; + + editor.on('SaveContent', function (e) { + lastEvent = e; + hiddenStateWhileSaving = editor.isHidden(); + }); + + editor.setContent('xyz'); + editor.hide(); + + const elm: any = document.getElementById(editor.id); + LegacyUnit.equal(hiddenStateWhileSaving, false, 'False isHidden state while saving'); + LegacyUnit.equal(lastEvent.content, 'xyz
'); + LegacyUnit.equal(elm.value, 'xyz
'); + + editor.show(); + }); + + suite.test('insertContent', function (editor) { + editor.setContent('ab
'); + LegacyUnit.setSelection(editor, 'p', 1); + editor.insertContent('c'); + LegacyUnit.equal(editor.getContent(), 'acb
'); + }); + + suite.test('insertContent merge', function (editor) { + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 1); + editor.insertContent('b', { merge: true }); + LegacyUnit.equal(editor.getContent(), 'ab
'); + }); + + suite.test('addCommand', function (editor) { + const scope = {}; + let lastScope, lastArgs; + + const callback = function () { + // eslint-disable-next-line + lastScope = this; + lastArgs = arguments; + }; + + editor.addCommand('CustomCommand1', callback, scope); + editor.addCommand('CustomCommand2', callback); + + editor.execCommand('CustomCommand1', false, 'value', { extra: true }); + LegacyUnit.equal(lastArgs[0], false); + LegacyUnit.equal(lastArgs[1], 'value'); + LegacyUnit.equal(lastScope === scope, true); + + editor.execCommand('CustomCommand2'); + LegacyUnit.equal(typeof lastArgs[0], 'undefined'); + LegacyUnit.equal(typeof lastArgs[1], 'undefined'); + LegacyUnit.equal(lastScope === editor, true); + }); + + suite.test('addQueryStateHandler', function (editor) { + const scope = {}; + let lastScope, currentState; + + const callback = function () { + // eslint-disable-next-line + lastScope = this; + return currentState; + }; + + editor.addQueryStateHandler('CustomCommand1', callback, scope); + editor.addQueryStateHandler('CustomCommand2', callback); + + currentState = false; + LegacyUnit.equal(editor.queryCommandState('CustomCommand1'), false); + LegacyUnit.equal(lastScope === scope, true, 'Scope is not custom scope'); + + currentState = true; + LegacyUnit.equal(editor.queryCommandState('CustomCommand2'), true); + LegacyUnit.equal(lastScope === editor, true, 'Scope is not editor'); + }); + + suite.test('Block script execution', function (editor) { + editor.setContent('x
'); + LegacyUnit.equal( + HtmlUtils.cleanHtml(editor.getBody().innerHTML), + '' + + '' + + '' + + 'x
' + ); + LegacyUnit.equal( + editor.getContent(), + '' + + '' + + '' + + 'x
' + ); + }); + + suite.test('addQueryValueHandler', function (editor) { + const scope = {}; + let lastScope, currentValue; + + const callback = function () { + // eslint-disable-next-line + lastScope = this; + return currentValue; + }; + + editor.addQueryValueHandler('CustomCommand1', callback, scope); + editor.addQueryValueHandler('CustomCommand2', callback); + + currentValue = 'a'; + LegacyUnit.equal(editor.queryCommandValue('CustomCommand1'), 'a'); + LegacyUnit.equal(lastScope === scope, true, 'Scope is not custom scope'); + + currentValue = 'b'; + LegacyUnit.equal(editor.queryCommandValue('CustomCommand2'), 'b'); + LegacyUnit.equal(lastScope === editor, true, 'Scope is not editor'); + }); + + suite.test('setDirty/isDirty', function (editor) { + let lastArgs = null; + + editor.on('dirty', function (e) { + lastArgs = e; + }); + + editor.setDirty(false); + LegacyUnit.equal(lastArgs, null); + LegacyUnit.equal(editor.isDirty(), false); + + editor.setDirty(true); + LegacyUnit.equal(lastArgs.type, 'dirty'); + LegacyUnit.equal(editor.isDirty(), true); + + lastArgs = null; + editor.setDirty(true); + LegacyUnit.equal(lastArgs, null); + LegacyUnit.equal(editor.isDirty(), true); + + editor.setDirty(false); + LegacyUnit.equal(lastArgs, null); + LegacyUnit.equal(editor.isDirty(), false); + }); + + suite.test('setMode', function (editor) { + let clickCount = 0; + + editor.on('click', function () { + clickCount++; + }); + + editor.dom.fire(editor.getBody(), 'click'); + LegacyUnit.equal(clickCount, 1); + + editor.setMode('readonly'); + LegacyUnit.equal(editor.theme.panel.find('button:last')[2].disabled(), true); + editor.dom.fire(editor.getBody(), 'click'); + LegacyUnit.equal(clickCount, 1); + + editor.setMode('design'); + editor.dom.fire(editor.getBody(), 'click'); + LegacyUnit.equal(editor.theme.panel.find('button:last')[2].disabled(), false); + LegacyUnit.equal(clickCount, 2); + }); + + suite.test('translate', function (editor) { + EditorManager.addI18n('en_US', { + 'input i18n': 'output i18n', + 'value:{0}{1}': 'value translation:{0}{1}' + }); + + LegacyUnit.equal(editor.translate('input i18n'), 'output i18n'); + LegacyUnit.equal(editor.translate(['value:{0}{1}', 'a', 'b']), 'value translation:ab'); + }); + + suite.test('Treat some paragraphs as empty contents', function (editor) { + editor.setContent('\u00a0
'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('kamer word boundaries', function (editor) { + editor.setContent('!\u200b!\u200b!
'); + LegacyUnit.equal(editor.getContent(), '!\u200b!\u200b!
'); + }); + + suite.test('Padd empty elements with br', function (editor) { + editor.settings.padd_empty_with_br = true; + editor.setContent('a
'); + LegacyUnit.equal(editor.getContent(), 'a
a
'); + LegacyUnit.setSelection(editor, 'p', 1); + editor.insertContent('b
'); + LegacyUnit.equal(editor.getContent(), 'a
b
'); + LegacyUnit.equal(editor.getContent(), '
'); + }); + + suite.test('hasFocus', function (editor) { + editor.focus(); + LegacyUnit.equal(editor.hasFocus(), true); + + const input = document.createElement('input'); + document.body.appendChild(input); + + input.focus(); + LegacyUnit.equal(editor.hasFocus(), false); + + editor.focus(); + LegacyUnit.equal(editor.hasFocus(), true); + + input.parentNode.removeChild(input); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), function () { + onSuccess(); + }, onFailure); + }, { + selector: 'textarea', + add_unload_trigger: false, + disable_nodechange: true, + custom_elements: 'custom1,~custom2', + extended_valid_elements: 'custom1,custom2,script[*]', + entities: 'raw', + indent: false, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorUploadTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorUploadTest.ts new file mode 100644 index 000000000..2e1cc4815 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/EditorUploadTest.ts @@ -0,0 +1,370 @@ +import { Pipeline, Step } from '@ephox/agar'; +import { Arr, Fun } from '@ephox/katamari'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import Env from 'tinymce/core/api/Env'; +import Conversions from 'tinymce/core/file/Conversions'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.EditorUploadTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + let testBlobDataUri; + + if (!Env.fileApi) { + return; + } + + const teardown = function (editor) { + return Step.sync(function () { + editor.editorUpload.destroy(); + editor.settings.automatic_uploads = false; + delete editor.settings.images_replace_blob_uris; + delete editor.settings.images_dataimg_filter; + }); + }; + + const appendTeardown = function (editor, steps) { + return Arr.bind(steps, function (step) { + return [step, teardown(editor)]; + }); + }; + + const imageHtml = function (uri) { + return DOMUtils.DOM.createHTML('img', { src: uri }); + }; + + const assertResult = function (editor, uploadedBlobInfo, result) { + LegacyUnit.strictEqual(result.length, 1); + LegacyUnit.strictEqual(result[0].status, true); + LegacyUnit.strictEqual(result[0].element.src.indexOf(uploadedBlobInfo.id() + '.png') !== -1, true); + LegacyUnit.equal('



a
'), + tinyApis.sFocus, + tinyApis.sSetCursor([0, 0], 0), + tinyApis.sNodeChanged, + sAssertSelectBoxDisplayValue(editor, 'Font Sizes', '24pt'), + sAssertSelectBoxDisplayValue(editor, 'Font Family', 'Arial') + ])), + + Logger.t('Font family and font size on paragraph with styles', GeneralSteps.sequence([ + tinyApis.sSetContent('a
'), + tinyApis.sFocus, + tinyApis.sSetCursor([0, 0], 0), + tinyApis.sNodeChanged, + // the following one should pick up 12.75pt, although there's a rounded 13pt in the dropdown as well + sAssertSelectBoxDisplayValue(editor, 'Font Sizes', '12.75pt'), + sAssertSelectBoxDisplayValue(editor, 'Font Family', 'Times') + ])) + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + toolbar: 'fontsizeselect fontselect', + content_style: [ + '.mce-content-body { font-family: Helvetica; font-size: 42px; }', + '.mce-content-body p { font-family: Arial; font-size: 32px; }' + ].join(''), + fontsize_formats: '12pt 12.75pt 13pt 32pt' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/ForceBlocksTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/ForceBlocksTest.ts new file mode 100644 index 000000000..bf330c0c5 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/ForceBlocksTest.ts @@ -0,0 +1,115 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import HtmlUtils from '../module/test/HtmlUtils'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.ForceBlocksTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + const pressArrowKey = function (editor) { + const dom = editor.dom, target = editor.selection.getNode(); + const evt = { keyCode: 37 }; + + dom.fire(target, 'keydown', evt); + dom.fire(target, 'keypress', evt); + dom.fire(target, 'keyup', evt); + }; + + suite.test('Wrap single root text node in P', function (editor) { + editor.focus(); + editor.getBody().innerHTML = 'abcd'; + LegacyUnit.setSelection(editor, 'body', 2); + pressArrowKey(editor); + LegacyUnit.equal(HtmlUtils.cleanHtml(editor.getBody().innerHTML), 'abcd
'); + LegacyUnit.equal(editor.selection.getNode().nodeName, 'P'); + }); + + suite.test('Wrap single root text node in P with attrs', function (editor) { + editor.settings.forced_root_block_attrs = { class: 'class1' }; + editor.getBody().innerHTML = 'abcd'; + LegacyUnit.setSelection(editor, 'body', 2); + pressArrowKey(editor); + LegacyUnit.equal(editor.getContent(), 'abcd
'); + LegacyUnit.equal(editor.selection.getNode().nodeName, 'P'); + delete editor.settings.forced_root_block_attrs; + }); + + suite.test('Wrap single root text node in P but not table sibling', function (editor) { + editor.getBody().innerHTML = 'abcd| x |
abcd
| x |
a
b
\nc
d
xe
'; + LegacyUnit.setSelection(editor, 'p', 0); + pressArrowKey(editor); + LegacyUnit.equal(HtmlUtils.cleanHtml(editor.getContent()), 'a
b
c
d
x
e
'); + }); + + suite.test('Do not wrap whitespace textnodes between inline elements', (editor) => { + editor.getBody().innerHTML = 'a b c'; + LegacyUnit.setSelection(editor, 'strong', 0); + pressArrowKey(editor); + LegacyUnit.equal(HtmlUtils.cleanHtml(editor.getContent()), 'a b c
'); + }); + + suite.test('Wrap root em in P but not table sibling', function (editor) { + editor.getBody().innerHTML = 'abcd| x |
abcd
| x |
1234
5678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[1].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
5678
', + 'selection of a list' + ); + }); + + suite.test('Toggle OFF - Inline element on selected text', function (editor) { + // Toggle OFF - Inline element on selected text + editor.formatter.register('format', { + inline: 'b', + toggle: false + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1234
'); + }); + + suite.test('Toggle OFF - Inline element on partially selected text', function (editor) { + // Toggle OFF - Inline element on partially selected text + editor.formatter.register('format', { + inline: 'b', + toggle: 0 + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 2); + editor.selection.setRng(rng); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1234
'); + }); + + suite.test('Toggle OFF - Inline element on partially selected text in start/end elements', function (editor) { + // Toggle OFF - Inline element on partially selected text in start/end elements + editor.formatter.register('format', { + inline: 'b', + toggle: false + }); + editor.getBody().innerHTML = '1234
1234
'; // '1234
1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[1].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1234
1234
'); + }); + + suite.test('Toggle OFF - Inline element with data attribute', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '1
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 1); + editor.selection.setRng(rng); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1
'); + }); + + suite.test('Toggle ON - NO inline element on selected text', function (editor) { + // Inline element on selected text + editor.formatter.register('format', { + inline: 'b', + toggle: true + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected text'); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Toggle ON - NO inline element on selected text'); + }); + + suite.test('Selection spanning from within format to outside format with toggle off', function (editor) { + editor.formatter.register('format', { + inline: 'b', + toggle: false + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].lastChild, 2); + editor.selection.setRng(rng); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Extend formating if start of selection is already formatted'); + }); + + suite.test('Inline element on partially selected text', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 1); + rng.setEnd(editor.dom.select('p')[0].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on partially selected text'); + editor.formatter.toggle('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Toggle ON - NO inline element on partially selected text'); + }); + + suite.test('Inline element on partially selected text in start/end elements', function (editor) { + // Inline element on partially selected text in start/end elements + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234
1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 1); + rng.setEnd(editor.dom.select('p')[1].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
1234
'); + }); + + suite.test('Inline element on selected element', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected element'); + }); + + suite.test('Inline element on multiple selected elements', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234
1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 2); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
1234
', 'Inline element on multiple selected elements'); + }); + + suite.test('Inline element on multiple selected elements with various childnodes', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '123456789
123456789
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 2); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '123456789
123456789
', + 'Inline element on multiple selected elements with various childnodes' + ); + }); + + suite.test('Inline element with attributes', function (editor) { + editor.formatter.register('format', { + inline: 'b', + attributes: { + title: 'value1', + id: 'value2' + } + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element with attributes'); + }); + + suite.test('Inline element with styles', function (editor) { + editor.formatter.register('format', { + inline: 'b', + styles: { + color: '#ff0000', + fontSize: '10px' + } + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element with styles'); + }); + + suite.test('Inline element with attributes and styles', function (editor) { + editor.formatter.register('format', { + inline: 'b', + attributes: { + title: 'value1', + id: 'value2' + }, + styles: { + color: '#ff0000', + fontSize: '10px' + } + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
', + 'Inline element with attributes and styles' + ); + }); + + suite.test('Inline element with wrapable parents', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = 'x1234y
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'x1234y
', 'Inline element with wrapable parents'); + }); + + suite.test('Inline element with redundant child', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element with redundant child'); + }); + + suite.test('Inline element with redundant parent', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0].firstChild, 0); + rng.setEnd(editor.dom.select('em')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a1234b
', 'Inline element with redundant parent'); + }); + + suite.test('Inline element with redundant child of similar type 1', function (editor) { + editor.formatter.register('format', [{ + inline: 'b' + }, { + inline: 'strong' + }]); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 3); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a1234b
', 'Inline element with redundant child of similar type 1'); + }); + + suite.test('Inline element with redundant child of similar type 2', function (editor) { + editor.formatter.register('format', [{ + inline: 'b' + }, { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }]); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element with redundant child of similar type 2'); + }); + + suite.test('Inline element with redundant children of similar types', function (editor) { + editor.formatter.register('format', [{ + inline: 'b' + }, { + inline: 'strong' + }, { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }]); + editor.getBody().innerHTML = 'a12345678b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a12345678b
', 'Inline element with redundant children of similar types'); + }); + + suite.test('Inline element with redundant parent 1', function (editor) { + editor.formatter.register('format', [{ + inline: 'b' + }, { + inline: 'strong' + }]); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0].firstChild, 0); + rng.setEnd(editor.dom.select('em')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a1234b
', 'Inline element with redundant parent 1'); + }); + + suite.test('Inline element with redundant parent 2', function (editor) { + editor.formatter.register('format', [{ + inline: 'b' + }, { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }]); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0].firstChild, 0); + rng.setEnd(editor.dom.select('em')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a1234b
', 'Inline element with redundant parent 2'); + }); + + suite.test('Inline element with redundant parents of similar types', function (editor) { + editor.formatter.register('format', [{ + inline: 'b' + }, { + inline: 'strong' + }, { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }]); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0].firstChild, 0); + rng.setEnd(editor.dom.select('em')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + 'a1234b
', + 'Inline element with redundant parents of similar types' + ); + }); + + suite.test('Inline element merged with parent and child', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = 'a123456b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 1); + rng.setEnd(editor.dom.select('b')[0].lastChild, 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a123456b
', 'Inline element merged with parent and child'); + }); + + suite.test('Inline element merged with child 1', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a1234b
', 'Inline element merged with child 1'); + }); + + suite.test('Inline element merged with child 2', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + 'a1234b
', + 'Inline element merged with child 2' + ); + }); + + suite.test('Inline element merged with child 3', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + fontWeight: 'bold' + } + }); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + 'a1234b
', + 'Inline element merged with child 3' + ); + }); + + suite.test('Inline element merged with child 3', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + fontWeight: 'bold' + }, + merge: true + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element merged with child 3'); + }); + + suite.test('Inline element merged with child 4', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + color: '#00ff00' + } + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element merged with child 4'); + }); + + suite.test('Inline element with attributes merged with child 1', function (editor) { + editor.formatter.register('format', { + inline: 'font', + attributes: { + face: 'arial' + }, + merge: true + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element with attributes merged with child 1'); + }); + + suite.test('Inline element with attributes merged with child 2', function (editor) { + editor.formatter.register('format', { + inline: 'font', + attributes: { + size: '7' + } + }); + editor.getBody().innerHTML = 'a1234b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a1234b
', 'Inline element with attributes merged with child 2'); + }); + + suite.test('Inline element merged with left sibling', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].lastChild, 0); + rng.setEnd(editor.dom.select('p')[0].lastChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '12345678
', 'Inline element merged with left sibling'); + }); + + suite.test('Inline element merged with right sibling', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '12345678
', 'Inline element merged with right sibling'); + }); + + suite.test('Inline element merged with left and right siblings', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '123456
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].childNodes[1], 0); + rng.setEnd(editor.dom.select('p')[0].childNodes[1], 2); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '123456
', 'Inline element merged with left and right siblings'); + }); + + suite.test('Inline element merged with data attributed left sibling', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].lastChild, 0); + rng.setEnd(editor.dom.select('p')[0].lastChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '12345678
', 'Inline element merged with left sibling'); + }); + + suite.test('Don\'t merge siblings with whitespace between 1', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = 'a b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].lastChild, 1); + rng.setEnd(editor.dom.select('p')[0].lastChild, 2); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a b
', 'Don\'t merge siblings with whitespace between 1'); + }); + + suite.test('Don\'t merge siblings with whitespace between 1', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = 'a b
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a b
', 'Don\'t merge siblings with whitespace between 2'); + }); + + suite.test('Inline element not merged in exact mode', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + color: '#00ff00' + }, + exact: true + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
', + 'Inline element not merged in exact mode' + ); + }); + + suite.test('Inline element merged in exact mode', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + color: '#ff0000' + }, + exact: true + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element merged in exact mode'); + }); + + suite.test('Deep left branch', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234text1text2
5678
9012
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('ins')[0].firstChild, 1); + rng.setEnd(editor.dom.select('p')[2].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234text1text2
5678
9012
', + 'Deep left branch' + ); + }); + + suite.test('Deep right branch', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '9012
5678
1234text1text2
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('em')[3].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '9012
5678
1234text1text2
', + 'Deep right branch' + ); + }); + + suite.test('Full element text selection on two elements with a table in the middle', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.getBody().innerHTML = '1234
| 123 |
5678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[1].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
| 123 |
5678
', + 'Full element text selection on two elements with a table in the middle' + ); + }); + + suite.test('Inline element on selected text with variables', function (editor) { + editor.formatter.register('format', { + inline: 'b', + styles: { + color: '%color' + }, + attributes: { + title: '%title' + } + }, { + color: '#ff0000', + title: 'title' + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format', { + color: '#ff0000', + title: 'title' + }); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected text'); + }); + + suite.test('Remove redundant children', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + fontFamily: 'arial' + } + }); + editor.getBody().innerHTML = ( + '1234
' + ); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Remove redundant children'); + }); + + suite.test('Inline element on selected text with function values', function (editor) { + editor.formatter.register('format', { + inline: 'b', + styles: { + color (vars) { + return vars.color + '00ff'; + } + }, + attributes: { + title (vars) { + return vars.title + '2'; + } + } + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format', { + color: '#ff', + title: 'title' + }); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected text with function values'); + }); + + suite.test('Block element on selected text', function (editor) { + editor.formatter.register('format', { + block: 'div' + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 1); + rng.setEnd(editor.dom.select('p')[0].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 1); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
5678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 2); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
5678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.getBody(), 0); + rng.setEnd(editor.getBody(), 2); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '1234
5678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('h1')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '', 'Block element wrapper 1'); + }); + + suite.test('Block element wrapper 2', function (editor) { + editor.formatter.register('format', { + block: 'blockquote', + wrapper: 1 + }); + editor.getBody().innerHTML = '1234
5678
', 'Block element wrapper 2'); + }); + + suite.test('Block element wrapper 3', function (editor) { + editor.formatter.register('format', { + block: 'blockquote', + wrapper: 1 + }); + editor.getBody().innerHTML = '1234
', 'Block element wrapper 3'); + }); + + suite.test('Apply format on single element that matches a selector 1', function (editor) { + editor.formatter.register('format', { + selector: 'p', + attributes: { + title: 'test' + }, + styles: { + color: '#ff0000' + }, + classes: 'a b c' + }); + editor.getBody().innerHTML = '1234
1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
', + 'Apply format on single element that matches a selector' + ); + }); + + suite.test('Apply format on single element parent that matches a selector 2', function (editor) { + editor.formatter.register('format', { + selector: 'div', + attributes: { + title: 'test' + }, + styles: { + color: '#ff0000' + }, + classes: 'a b c' + }); + editor.getBody().innerHTML = '1234
test
1234
1234
test
1234
1234
1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[1].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
1234
', + 'Apply format on multiple elements that matches a selector' + ); + }); + + suite.test('Apply format on top of existing selector element', function (editor) { + editor.formatter.register('format', { + selector: 'p', + attributes: { + title: 'test2' + }, + styles: { + color: '#00ff00' + }, + classes: 'a b c' + }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + getContent(editor), + '1234
', + 'Apply format on top of existing selector element' + ); + }); + + suite.test('Format on single li that matches a selector', function (editor) { + editor.formatter.register('format', { + inline: 'span', + selector: 'li', + attributes: { + title: 'test' + }, + styles: { + color: '#ff0000' + }, + classes: 'a b c' + }); + editor.getBody().innerHTML = 'test1 test2 test3 test4 test5 test6
'); + rng.setStart(editor.dom.select('strong')[0].firstChild, 6); + rng.setEnd(editor.dom.select('strong')[0].firstChild, 11); + editor.focus(); + editor.selection.setRng(rng); + editor.execCommand('Italic'); + LegacyUnit.equal( + editor.getContent(editor), + 'test1 test2 test3 test4 test5 test6
', + 'Selected text should be bold.' + ); + }); + + suite.test('Apply color format to links as well', function (editor) { + editor.setContent('123abc456
'); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].lastChild, 3); + editor.selection.setRng(rng); + + editor.formatter.register('format', { + inline: 'span', + styles: { + color: '#FF0000' + }, + links: true + }); + editor.formatter.apply('format'); + + LegacyUnit.equal( + editor.getContent(editor), + '123abc456
', + 'Link should have it\'s own color.' + ); + }); + + suite.test('Color on link element', function (editor) { + editor.setContent('123abc456
'); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].lastChild, 3); + editor.selection.setRng(rng); + + editor.formatter.register('format', { + inline: 'span', + styles: { + color: '#FF0000' + }, + links: true + }); + editor.formatter.apply('format'); + + LegacyUnit.equal( + editor.getContent(editor), + '123abc456
', + 'Link should have it\'s own color.' + ); + }); + + suite.test('Applying formats in lists', function (editor) { + editor.setContent('test
'); + editor.execCommand('SelectAll'); + editor.formatter.apply('format'); + LegacyUnit.equal( + editor.getContent(editor), + 'test
', + 'Coloring an underlined text should result in a colored underline' + ); + }); + + suite.test('Underline colors 2', function (editor) { + editor.formatter.register('format', { + inline: 'span', + exact: true, + styles: { + textDecoration: 'underline' + } + }); + editor.setContent('test
'); + editor.execCommand('SelectAll'); + editor.formatter.apply('format'); + LegacyUnit.equal( + editor.getContent(editor), + 'test
', + 'Underlining colored text should result in a colored underline' + ); + }); + + suite.test('Underline colors 3', function (editor) { + editor.formatter.register('format', { + inline: 'span', + exact: true, + styles: { + textDecoration: 'underline' + } + }); + editor.setContent( + 'This is some ' + + 'example text
' + ); + editor.execCommand('SelectAll'); + editor.formatter.apply('format'); + LegacyUnit.equal( + editor.getContent(editor), + '' + + 'This is some example' + + ' text
', 'Underlining colored and underlined text should result in a colored underline' + ); + }); + + suite.test('Underline colors 4', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + color: '#ff0000' + } + }); + editor.setContent( + 'yellowredyellow
' + ); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[1].firstChild, 6); + rng.setEnd(editor.dom.select('span')[1].firstChild, 9); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'yellowredyellow
', + 'Coloring an colored underdlined text should result in newly colored underline' + ); + }); + + suite.test('Underline colors 5', function (editor) { + editor.formatter.register('format', { + inline: 'span', + exact: true, + styles: { + textDecoration: 'underline' + } + }); + editor.setContent( + 'This is some example text
This is some example' + + ' text
This is' + + ' some example text
' + ); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('strong')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[4].lastChild, 5); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal( + editor.getContent(editor), + 'This is some example text
This is some example text
This is some example text
', + 'Colored elements should be underlined when selection is across multiple paragraphs' + ); + }); + + suite.test('Underline colors 6', function (editor) { + editor.formatter.register('format', { + inline: 'span', + exact: true, + styles: { + color: '#ff0000' + } + }); + editor.setContent('This is some text.
'); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 8); + rng.setEnd(editor.dom.select('span')[0].firstChild, 12); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + editor.formatter.remove('format'); + LegacyUnit.equal( + editor.getContent(editor), + 'This is some text.
', + 'Children nodes that are underlined should be removed if their parent nodes are underlined' + ); + }); + + suite.test('Underline colors 7', function (editor) { + editor.formatter.register('format', { + inline: 'span', + exact: true, + styles: { + color: '#ff0000' + } + }); + editor.setContent( + 'This is some text.
' + ); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[1].firstChild, 0); + rng.setEnd(editor.dom.select('span')[1].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal( + editor.getContent(editor), + 'This is ' + + 'some text.
', + 'Children nodes that are underlined should be removed if their parent nodes are underlined' + ); + }); + + suite.test('Caret format inside single block word', function (editor) { + editor.setContent('abc
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 2, 'p', 2); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
'); + }); + + suite.test('Caret format inside non-ascii single block word', function (editor) { + editor.setContent('noël
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 2, 'p', 2); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'noël
'); + }); + + suite.test('Caret format inside first block word', function (editor) { + editor.setContent('abc 123
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 2, 'p', 2); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc 123
'); + }); + + suite.test('Caret format inside last block word', function (editor) { + editor.setContent('abc 123
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 5, 'p', 5); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc 123
'); + }); + + suite.test('Caret format inside middle block word', function (editor) { + editor.setContent('abc 123 456
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 5, 'p', 5); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc 123 456
'); + }); + + suite.test('Caret format on word separated by non breaking space', function (editor) { + editor.setContent('one two
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 1, 'p', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'one\u00a0two
'); + }); + + suite.test('Caret format inside single inline wrapped word', function (editor) { + editor.setContent('abc 123 456
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'em', 1, 'em', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc 123 456
'); + }); + + suite.test('Caret format inside word before similar format', function (editor) { + editor.setContent('abc 123 456
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 1, 'p', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc 123 456
'); + }); + + suite.test('Caret format inside last inline wrapped word', function (editor) { + editor.setContent('abc abc 123 456
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'em', 5, 'em', 5); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc abc 123 456
'); + }); + + suite.test('Caret format before text', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 0); + editor.formatter.apply('format'); + KeyUtils.type(editor, 'b'); + LegacyUnit.equal(editor.getContent(editor), 'ba
'); + }); + + suite.test('Caret format after text', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 1, 'p', 1); + editor.formatter.apply('format'); + KeyUtils.type(editor, 'b'); + LegacyUnit.equal(editor.getContent(editor), 'ab
'); + }); + + suite.test('Caret format and no key press', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 0); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'a
'); + }); + + suite.test('Caret format and arrow left', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 0); + editor.formatter.apply('format'); + KeyUtils.type(editor, { + keyCode: 37 + }); + LegacyUnit.equal(editor.getContent(editor), 'a
'); + }); + + suite.test('Caret format and arrow right', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'b' + }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 0); + editor.formatter.apply('format'); + KeyUtils.type(editor, { + keyCode: 39 + }); + LegacyUnit.equal(editor.getContent(editor), 'a
'); + }); + + suite.test('Caret format and backspace', function (editor) { + let rng; + + editor.formatter.register('format', { + inline: 'b' + }); + + editor.setContent('abc
'); + rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 3); + rng.setEnd(editor.dom.select('p')[0].firstChild, 3); + editor.selection.setRng(rng); + + editor.formatter.apply('format'); + KeyUtils.type(editor, '\b'); + LegacyUnit.equal(editor.getContent(editor), 'ab
'); + }); + + suite.test('Caret format on word in li with word in parent li before it', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'span', + selector: '*', + classes: 'test' + }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'a
'); + }); + + suite.test('format inline on contentEditable: false block', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.setContent('abc
def
'); + editor.selection.select(editor.getBody().childNodes[1]); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
', 'Text is not bold'); + }); + + suite.test('format block on contentEditable: false block', function (editor) { + editor.formatter.register('format', { + block: 'h1' + }); + editor.setContent('abc
def
'); + editor.selection.select(editor.getBody().childNodes[1]); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
', 'P is not h1'); + }); + + suite.test('contentEditable: false on start and contentEditable: true on end', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.setContent('abc
def
ghi
'); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[2].firstChild, 0); + rng.setEnd(editor.dom.select('p')[2].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
ghi
', 'Text in last paragraph is bold'); + }); + + suite.test('contentEditable: true on start and contentEditable: false on end', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.setContent('abc
def
'); + LegacyUnit.setSelection(editor, 'p:nth-child(1)', 0, 'p:nth-child(2)', 3); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
', 'Text in first paragraph is bold'); + }); + + suite.test('contentEditable: true inside contentEditable: false', function (editor) { + editor.formatter.register('format', { + inline: 'b' + }); + editor.setContent('abc
def
'); + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
', 'Text is bold'); + }); + + suite.test('Del element wrapping blocks', function (editor) { + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.formatter.register('format', { + block: 'del', + wrapper: true + }); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a
a
'); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.formatter.register('format', { + block: 'del' + }); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a
'); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.formatter.register('format', { + inline: 'del' + }); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a
| a |
| a |
a' +
+ '
| ' +
+ '
a' +
+ '
| ' +
+ '
ab
'; + + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].lastChild, 1); + editor.selection.setRng(rng); + + editor.formatter.register('format', { + inline: 'span', + attributes: { + id: 'id' + } + }); + editor.formatter.apply('format'); + + LegacyUnit.equal(HtmlUtils.normalizeHtml(editor.getBody().innerHTML), 'ab
'); + }); + + suite.test('Bug #5134 - TinyMCE removes formatting tags in the getContent', function (editor) { + editor.setContent(''); + editor.formatter.register('format', { + inline: 'strong', + toggle: false + }); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '', 'empty TinyMCE'); + editor.selection.setContent('a'); + LegacyUnit.equal(getContent(editor), 'a', 'bold text inside TinyMCE'); + }); + + suite.test('Bug #5134 - TinyMCE removes formatting tags in the getContent - typing', function (editor) { + editor.setContent(''); + editor.formatter.register('format', { + inline: 'strong', + toggle: false + }); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), '', 'empty TinyMCE'); + KeyUtils.type(editor, 'a'); + LegacyUnit.equal(getContent(editor), 'a', 'bold text inside TinyMCE'); + }); + + suite.test('Bug #5453 - TD contents with BR gets wrapped in block format', function (editor) { + editor.setContent('| abc 123 |
abc123 |
abc
'); + LegacyUnit.setSelection(editor, 'p', 2, 'p', 3); + editor.formatter.apply('format'); + LegacyUnit.setSelection(editor, 'p', 1, 'p', 2); + editor.formatter.apply('format'); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
'); + }); + + suite.test('merge_with_parents', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + fontWeight: 'bold' + }, + merge_with_parents: true + }); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'span', 0, 'span', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(editor.getContent(editor), 'a
'); + }); + + suite.test('Format selection from with end at beginning of block', function (editor) { + editor.setContent('abcd
'); + LegacyUnit.setSelection(editor, 'strong', 1, 'em', 0); + editor.formatter.apply('underline'); + LegacyUnit.equal(getContent(editor), 'abcd
'); + }); + + suite.test('Child wrapper having the same format as the immediate parent, shouldn\'t be removed if it also has other formats merged', function (editor) { + editor.getBody().innerHTML = 'a bc
'; + LegacyUnit.setSelection(editor, 'span span', 0, 'span span', 1); + editor.formatter.apply('fontname', { value: 'verdana' }); + LegacyUnit.equal(getContent(editor), 'a bc
'); + }); + + suite.test('FontName should not toggle', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'p', 0, 'p', 3); + editor.formatter.toggle('fontname', { value: 'arial' }); + LegacyUnit.equal(getContent(editor), 'abc
'); + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.toggle('fontname', { value: 'arial' }); + LegacyUnit.equal(getContent(editor), 'abc
'); + }); + + suite.test('FontSize should not toggle', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'p', 0, 'p', 3); + editor.formatter.toggle('fontsize', { value: '14pt' }); + LegacyUnit.equal(getContent(editor), 'abc
'); + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.toggle('fontsize', { value: '14pt' }); + LegacyUnit.equal(getContent(editor), 'abc
'); + }); + + suite.test('All the nested childNodes having fontSize should receive backgroundColor as well', function (editor) { + editor.getBody().innerHTML = 'a b c
'; + editor.selection.select(editor.dom.select('p')[0]); + + editor.formatter.apply('hilitecolor', { value: '#ff0000' }); + LegacyUnit.equal( + getContent(editor), + 'a b c
' + ); + + editor.formatter.remove('hilitecolor', { value: '#ff0000' }); + LegacyUnit.equal(getContent(editor), 'a b c
'); + }); + + suite.test('Formatter should wrap elements that have data-mce-bogus attribute, rather then attempt to inject styles into it', function (editor) { + // add a class to retain bogus element + editor.getBody().innerHTML = 'That is a misespelled text
'; + editor.selection.select(editor.dom.select('span')[0]); + + editor.formatter.apply('fontname', { value: 'verdana' }); + + LegacyUnit.equal(editor.getBody().innerHTML, + 'That is a misespelled text
'); + + LegacyUnit.equal(getContent(editor), + 'that is a misespelled text
'); + + editor.selection.select(editor.dom.select('span')[0]); + editor.formatter.remove('fontname', { value: 'verdana' }); + + LegacyUnit.equal(editor.getBody().innerHTML, + 'That is a misespelled text
'); + + LegacyUnit.equal(getContent(editor), + 'that is a misespelled text
'); + }); + + suite.test('TINY-1180: Formatting gets applied outside the currently selected range', function (editor) { + editor.getBody().innerHTML = 'a em
'; + LegacyUnit.setSelection(editor, 'p', 0, 'em em', 0); + editor.formatter.apply('strikethrough'); + LegacyUnit.equal(getContent(editor), 'a em
'); + }); + + suite.test('Superscript on subscript removes the subscript element', function (editor) { + editor.getBody().innerHTML = 'a
'; + LegacyUnit.setSelection(editor, 'sub', 0, 'sub', 1); + editor.formatter.apply('superscript'); + LegacyUnit.equal(getContent(editor), 'a
'); + }); + + suite.test('Subscript on superscript removes the superscript element', function (editor) { + editor.getBody().innerHTML = 'a
'; + LegacyUnit.setSelection(editor, 'sup', 0, 'sup', 1); + editor.formatter.apply('subscript'); + LegacyUnit.equal(getContent(editor), 'a
'); + }); + + suite.test('TINY-782: Can\'t apply sub/sup to word on own line with large font', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.apply('superscript'); + LegacyUnit.equal(getContent(editor), 'abc
'); + }); + + suite.test('TINY-782: Apply sub/sup to range with multiple font sizes', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'p', 0, 'span:nth-child(2)', 1); + editor.formatter.apply('superscript'); + LegacyUnit.equal(getContent(editor), 'abc
'); + }); + + suite.test('TINY-671: Background color on nested font size bug', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.apply('hilitecolor', { value: '#ff0000' }); + LegacyUnit.equal(getContent(editor), 'abc
'); + }); + + suite.test('Background color over range of font sizes', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'p', 0, 'span:nth-child(2)', 1); + editor.formatter.apply('hilitecolor', { value: '#ff0000' }); + LegacyUnit.equal( + getContent(editor), + 'abc
' + ); + }); + + suite.test('TINY-865: Font size removed when changing background color', function (editor) { + editor.getBody().innerHTML = ( + 'a ' + + 'b c
' + ); + LegacyUnit.setSelection(editor, 'span span:nth-child(2)', 0, 'span span:nth-child(2)', 1); + editor.formatter.apply('hilitecolor', { value: '#ff0000' }); + LegacyUnit.equal( + getContent(editor), + 'a b c
' + ); + }); + + suite.test('TINY-935: Text color, then size, then change color wraps span doesn\'t change color', function (editor) { + editor.getBody().innerHTML = 'text
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 4); + editor.formatter.apply('forecolor', { value: '#ff0000' }); + LegacyUnit.equal(getContent(editor), 'text
'); + }); + + suite.test('GH-3519: Font family selection does not work after changing font size', function (editor) { + editor.getBody().innerHTML = 'text
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 4); + editor.formatter.apply('fontname', { value: 'verdana' }); + LegacyUnit.equal(getContent(editor), 'text
'); + }); + + suite.test('Formatter should remove similar styles when clear_child_styles is set to true', function (editor) { + editor.getBody().innerHTML = ( + 'a' +
+ 'b' +
+ 'c
abc
abc
'; + + editor.selection.select(editor.dom.select('p')[0]); + + editor.formatter.register('format', { inline: 'span', styles: { fontSize: '14px' }, links: true, clear_child_styles: true }); + editor.formatter.apply('format'); + + LegacyUnit.equal( + getContent(editor), + 'abc
' + ); + }); + + suite.test('Formatter should remove similar styles when clear_child_styles isn\'t defined', function (editor) { + editor.getBody().innerHTML = ( + 'a' +
+ 'b' +
+ 'c
abc
a
a
a
a b
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 2); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a b
'); + }); + + suite.test('Apply format with onformat handler', function (editor) { + editor.setContent('a
'); + editor.formatter.register('format', { + inline: 'span', + onformat (elm) { + elm.className = 'x'; + } + }); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.formatter.apply('format'); + LegacyUnit.equal(getContent(editor), 'a
'); + }); + + suite.test('Apply format to triple clicked selection (webkit)', function (editor) { + editor.setContent('a
a
abcdef
'); + LegacyUnit.setSelection(editor, 'span span', 1, 'strong', 1); + editor.formatter.apply('hilitecolor', { value: '#00ff00' }); + LegacyUnit.equal(getContent(editor), 'abcdef
'); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + indent: false, + extended_valid_elements: 'b[id|style|title],i[id|style|title],span[id|class|style|title|contenteditable],font[face|size]', + entities: 'raw', + convert_fonts_to_spans: false, + forced_root_block: false, + 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,text-align' + }, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/FormatterCheckTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/FormatterCheckTest.ts new file mode 100644 index 000000000..e962435f6 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/FormatterCheckTest.ts @@ -0,0 +1,226 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.FormatterCheckTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + suite.test('Selected style element text', function (editor) { + editor.focus(); + editor.formatter.register('bold', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('bold'), true, 'Selected style element text'); + }); + + suite.test('Selected style element with css styles', function (editor) { + editor.formatter.register('color', { inline: 'span', styles: { color: '#ff0000' } }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('color'), true, 'Selected style element with css styles'); + }); + + suite.test('Selected style element with css styles indexed', function (editor) { + editor.formatter.register('color', { inline: 'span', styles: ['color'] }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('color'), true, 'Selected style element with css styles'); + }); + + suite.test('Selected style element with attributes', function (editor) { + editor.formatter.register('fontsize', { inline: 'font', attributes: { size: '7' } }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('font')[0].firstChild, 0); + rng.setEnd(editor.dom.select('font')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('fontsize'), true, 'Selected style element with attributes'); + }); + + suite.test('Selected style element text multiple formats', function (editor) { + editor.formatter.register('multiple', [ + { inline: 'b' }, + { inline: 'strong' } + ]); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('strong')[0].firstChild, 0); + rng.setEnd(editor.dom.select('strong')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('multiple'), true, 'Selected style element text multiple formats'); + }); + + suite.test('Selected complex style element', function (editor) { + editor.formatter.register('complex', { inline: 'span', styles: { fontWeight: 'bold' } }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('complex'), true, 'Selected complex style element'); + }); + + suite.test('Selected non style element text', function (editor) { + editor.formatter.register('bold', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('bold'), false, 'Selected non style element text'); + }); + + suite.test('Selected partial style element (start)', function (editor) { + editor.formatter.register('bold', { inline: 'b' }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].lastChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('bold'), true, 'Selected partial style element (start)'); + }); + + suite.test('Selected partial style element (end)', function (editor) { + editor.formatter.register('bold', { inline: 'b' }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].lastChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('bold'), false, 'Selected partial style element (end)'); + }); + + suite.test('Selected element text with parent inline element', function (editor) { + editor.formatter.register('bold', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('bold'), true, 'Selected element text with parent inline element'); + }); + + suite.test('Selected element match with variable', function (editor) { + editor.formatter.register('complex', { inline: 'span', styles: { color: '%color' } }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('complex', { color: '#ff0000' }), true, 'Selected element match with variable'); + }); + + suite.test('Selected element match with variable and function', function (editor) { + editor.formatter.register('complex', { + inline: 'span', + styles: { + color (vars) { + return vars.color + '00'; + } + } + }); + + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 0); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('complex', { color: '#ff00' }), true, 'Selected element match with variable and function'); + }); + + suite.test('matchAll', function (editor) { + editor.getBody().innerHTML = 'a
'; + LegacyUnit.setSelection(editor, 'i', 0, 'i', 1); + LegacyUnit.equal(editor.formatter.matchAll(['bold', 'italic', 'underline']), ['italic', 'bold']); + }); + + suite.test('canApply', function (editor) { + editor.getBody().innerHTML = 'a
'; + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + LegacyUnit.equal(editor.formatter.canApply('bold'), true); + }); + + suite.test('Custom onmatch handler', function (editor) { + editor.formatter.register('format', { + inline: 'span', + onmatch (elm) { + return elm.className === 'x'; + } + }); + + editor.setContent('ab
'); + LegacyUnit.setSelection(editor, 'span:nth-child(1)', 0, 'span:nth-child(1)', 0); + LegacyUnit.equal(editor.formatter.match('format'), false, 'Should not match since the onmatch matches on x classes.'); + LegacyUnit.setSelection(editor, 'span:nth-child(2)', 0, 'span:nth-child(2)', 0); + LegacyUnit.equal(editor.formatter.match('format'), true, 'Should match since the onmatch matches on x classes.'); + }); + + suite.test('formatChanged complex format', function (editor) { + let newState, newArgs; + + editor.formatter.register('complex', { inline: 'span', styles: { color: '%color' } }); + + editor.formatter.formatChanged('complex', function (state, args) { + newState = state; + newArgs = args; + }, true); + + editor.getBody().innerHTML = 'text
'; + LegacyUnit.setSelection(editor, 'p', 0, 'p', 4); + + // Check apply + editor.formatter.apply('complex', { color: '#FF0000' }); + editor.nodeChanged(); + LegacyUnit.equal(newState, true); + LegacyUnit.equal(newArgs.format, 'complex'); + LegacyUnit.equalDom(newArgs.node, editor.getBody().firstChild.firstChild); + LegacyUnit.equal(newArgs.parents.length, 2); + + // Check remove + editor.formatter.remove('complex', { color: '#FF0000' }); + editor.nodeChanged(); + LegacyUnit.equal(newState, false); + LegacyUnit.equal(newArgs.format, 'complex'); + LegacyUnit.equalDom(newArgs.node, editor.getBody().firstChild); + LegacyUnit.equal(newArgs.parents.length, 1); + }); + + suite.test('Selected style element text', function (editor) { + editor.formatter.register('bold', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 4); + editor.selection.setRng(rng); + LegacyUnit.equal(editor.formatter.match('bold'), true, 'Selected style element text'); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + indent: false, + extended_valid_elements: 'b,i,span[style|class|contenteditable]', + entities: 'raw', + convert_fonts_to_spans: false, + forced_root_block: false, + 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,text-align' + }, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/FormatterRemoveTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/FormatterRemoveTest.ts new file mode 100644 index 000000000..fd0d6ea47 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/FormatterRemoveTest.ts @@ -0,0 +1,499 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import Env from 'tinymce/core/api/Env'; +import HtmlUtils from '../module/test/HtmlUtils'; +import KeyUtils from '../module/test/KeyUtils'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.FormatterRemoveTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + const getContent = function (editor) { + return editor.getContent(editor).toLowerCase().replace(/[\r]+/g, ''); + }; + + suite.test('Inline element on selected text', function (editor) { + editor.focus(); + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected text'); + }); + + suite.test('Inline element on selected text with remove=all', function (editor) { + editor.formatter.register('format', { selector: 'b', remove: 'all' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected text with remove=all'); + }); + + suite.test('Inline element on selected text with remove=none', function (editor) { + editor.formatter.register('format', { selector: 'span', styles: { fontWeight: 'bold' }, remove: 'none' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 1); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Inline element on selected text with remove=none'); + }); + + suite.test('Inline element style where element is format root', function (editor) { + editor.formatter.register('format', { inline: 'span', styles: { fontWeight: 'bold' } }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0].firstChild, 1); + rng.setEnd(editor.dom.select('em')[0].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), + '' + + '123' + + '4' + + '
', + 'Inline element style where element is format root'); + }); + + suite.test('Partially selected inline element text', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 2); + rng.setEnd(editor.dom.select('b')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Partially selected inline element text'); + }); + + suite.test('Partially selected inline element text with children', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[0].firstChild, 2); + rng.setEnd(editor.dom.select('span')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal( + getContent(editor), + '1234
', + 'Partially selected inline element text with children' + ); + }); + + suite.test('Partially selected inline element text with complex children', function (editor) { + editor.formatter.register('format', { inline: 'span', styles: { fontWeight: 'bold' } }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('span')[1].firstChild, 2); + rng.setEnd(editor.dom.select('span')[1].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal( + getContent(editor), + '12' + + '34
', + 'Partially selected inline element text with complex children' + ); + }); + + suite.test('Inline elements with exact flag', function (editor) { + editor.formatter.register('format', { inline: 'span', styles: { color: '#ff0000' }, exact: true }); + editor.getBody().innerHTML = '12341234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 2); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal( + getContent(editor), + '12341234
', + 'Inline elements with exact flag' + ); + }); + + suite.test('Inline elements with variables', function (editor) { + editor.formatter.register('format', { inline: 'span', styles: { color: '%color' }, exact: true }); + editor.getBody().innerHTML = '12341234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 2); + editor.selection.setRng(rng); + editor.formatter.remove('format', { color: '#ff0000' }); + LegacyUnit.equal( + getContent(editor), + '12341234
', + 'Inline elements on selected text with variables' + ); + }); + + suite.test('Inline elements with functions and variables', function (editor) { + editor.formatter.register('format', { + inline: 'span', + styles: { + color (vars) { + return vars.color + '00'; + } + }, + exact: true + }); + + editor.getBody().innerHTML = '12341234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('p')[0], 2); + editor.selection.setRng(rng); + editor.formatter.remove('format', { + color: '#ff00' + }); + LegacyUnit.equal( + getContent(editor), + '12341234
', + 'Inline elements with functions and variables' + ); + }); + + suite.test('End within start element', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('b')[0], 2); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '12345678
', 'End within start element'); + }); + + suite.test('Start and end within similar format 1', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0], 0); + rng.setEnd(editor.dom.select('b')[1], 2); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '12345678
', 'Start and end within similar format 1'); + }); + + suite.test('Start and end within similar format 2', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '12345678
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0], 0); + rng.setEnd(editor.dom.select('em')[0], 1); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '12345678
', 'Start and end within similar format 2'); + }); + + suite.test('Start and end within similar format 3', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = '1234
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('em')[0], 0); + rng.setEnd(editor.dom.select('em')[0], 1); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), '1234
', 'Start and end within similar format 3'); + }); + + suite.test('End within start', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = 'xabcy
'; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0], 0); + rng.setEnd(editor.dom.select('b')[1].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), 'xabcy
', 'End within start'); + }); + + suite.test('Remove block format', function (editor) { + editor.formatter.register('format', { block: 'h1' }); + editor.getBody().innerHTML = 'text
', 'Remove block format'); + }); + + suite.test('Remove wrapper block format', function (editor) { + editor.formatter.register('format', { block: 'blockquote', wrapper: true }); + editor.getBody().innerHTML = ''; + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 0); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), 'text
text
', 'Remove wrapper block format'); + }); + + suite.test('Remove span format within block with style', function (editor) { + editor.formatter.register('format', { selector: 'span', attributes: ['style', 'class'], remove: 'empty', split: true, expand: false, deep: true }); + const rng = editor.dom.createRng(); + editor.getBody().innerHTML = 'text
'; + rng.setStart(editor.dom.select('span')[0].firstChild, 1); + rng.setEnd(editor.dom.select('span')[0].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal( + getContent(editor), + 'text
', + 'Remove span format within block with style' + ); + }); + + suite.test('Remove and verify start element', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + const rng = editor.dom.createRng(); + editor.getBody().innerHTML = 'text
'; + rng.setStart(editor.dom.select('b')[0].firstChild, 1); + rng.setEnd(editor.dom.select('b')[0].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), 'text
'); + LegacyUnit.equal(editor.selection.getStart().nodeName, 'P'); + }); + + suite.test('Remove with selection collapsed ensure correct caret position', function (editor) { + const content = 'test
testing
'; + + editor.formatter.register('format', { block: 'p' }); + const rng = editor.dom.createRng(); + editor.getBody().innerHTML = content; + rng.setStart(editor.dom.select('p')[0].firstChild, 4); + rng.setEnd(editor.dom.select('p')[0].firstChild, 4); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), content); + LegacyUnit.equalDom(editor.selection.getStart(), editor.dom.select('p')[0]); + }); + + suite.test('Caret format at middle of text', function (editor) { + editor.setContent('abc
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'b', 1, 'b', 1); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
'); + }); + + suite.test('Caret format at end of text', function (editor) { + editor.setContent('abc
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'b', 3, 'b', 3); + editor.formatter.remove('format'); + KeyUtils.type(editor, 'd'); + LegacyUnit.equal(editor.getContent(editor), 'abcd
'); + }); + + suite.test('Caret format at end of text inside other format', function (editor) { + editor.setContent('abc
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'b', 3, 'b', 3); + editor.formatter.remove('format'); + KeyUtils.type(editor, 'd'); + LegacyUnit.equal(editor.getContent(editor), 'abcd
'); + }); + + suite.test('Caret format at end of text inside other format with text after 1', function (editor) { + editor.setContent('abce
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'b', 3, 'b', 3); + editor.formatter.remove('format'); + KeyUtils.type(editor, 'd'); + LegacyUnit.equal(editor.getContent(editor), 'abcde
'); + }); + + suite.test('Caret format at end of text inside other format with text after 2', function (editor) { + editor.setContent('abce
'); + editor.formatter.register('format', { inline: 'em' }); + LegacyUnit.setSelection(editor, 'b', 3, 'b', 3); + editor.formatter.remove('format'); + KeyUtils.type(editor, 'd'); + LegacyUnit.equal(editor.getContent(editor), 'abcde
'); + }); + + suite.test('Toggle styles at the end of the content don\' removes the format where it is not needed.', function (editor) { + editor.setContent('abce
'); + editor.formatter.register('b', { inline: 'b' }); + editor.formatter.register('em', { inline: 'em' }); + LegacyUnit.setSelection(editor, 'b', 4, 'b', 4); + editor.formatter.remove('b'); + editor.formatter.remove('em'); + LegacyUnit.equal(editor.getContent(editor), 'abce
'); + }); + + suite.test('Caret format on second word in table cell', function (editor) { + editor.setContent('| one two |
| one two |
abc
def
ghj
'); + const rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('b')[0].firstChild, 0); + rng.setEnd(editor.dom.select('b')[1].firstChild, 3); + editor.selection.setRng(rng); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
ghj
', 'Text in last paragraph is not bold'); + }); + + suite.test('contentEditable: true on start and contentEditable: false on end', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.setContent('abc
def
ghj
'); + LegacyUnit.setSelection(editor, 'p:nth-child(2) b', 0, 'p:last b', 3); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
ghj
', 'Text in first paragraph is not bold'); + }); + + suite.test('contentEditable: true inside contentEditable: false', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.setContent('abc
def
'); + LegacyUnit.setSelection(editor, 'b', 0, 'b', 3); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
def
', 'Text is not bold'); + }); + + suite.test('remove format block on contentEditable: false block', function (editor) { + editor.formatter.register('format', { block: 'h1' }); + editor.setContent('abc
abc
abc
abc
'); + }); + + suite.test('remove format on span with class using removeformat format', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.remove('removeformat'); + LegacyUnit.equal(HtmlUtils.cleanHtml(editor.getBody().innerHTML), 'abc
'); + }); + + suite.test('remove format on span with internal class using removeformat format', function (editor) { + editor.getBody().innerHTML = 'abc
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 3); + editor.formatter.remove('removeformat'); + LegacyUnit.equal(HtmlUtils.normalizeHtml(HtmlUtils.cleanHtml(editor.getBody().innerHTML)), 'abc
'); + }); + + suite.test('Remove format bug 1', function (editor) { + editor.setContent('abc
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'i', 1, 'i', 2); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
'); + }); + + suite.test('Remove format bug 2', function (editor) { + editor.setContent('abc
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'b', 0, 'b', 1); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'abc
'); + }); + + suite.test('Remove format bug 3', function (editor) { + editor.setContent('ab
'); + editor.formatter.register('format', { inline: 'b' }); + LegacyUnit.setSelection(editor, 'i', 1, 'i', 2); + editor.formatter.remove('format'); + LegacyUnit.equal(editor.getContent(editor), 'ab
'); + }); + + suite.test('Remove format with classes', function (editor) { + editor.formatter.register('format', { inline: 'span', classes: ['a', 'b'] }); + editor.getBody().innerHTML = 'a
'; + LegacyUnit.setSelection(editor, 'span', 0, 'span', 1); + editor.formatter.remove('format'); + LegacyUnit.equal(getContent(editor), 'a
', 'Element should only have c left'); + }); + + suite.test('Remove format on specified node', function (editor) { + editor.formatter.register('format', { inline: 'b' }); + editor.getBody().innerHTML = 'a
'; + editor.formatter.remove('format', {}, editor.dom.select('b')[0]); + LegacyUnit.equal(getContent(editor), 'a
', 'B should be removed'); + }); + + suite.test('Remove ceFalseOverride format', function (editor) { + editor.setContent('a
a
a
| ab cd |
| ab cd |
test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('Italic'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('Underline'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('Strikethrough'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('FontName', false, 'Arial'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('FontSize', false, '7'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('FontSize', false, '7pt'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('ForeColor', false, '#FF0000'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('HiliteColor', false, '#FF0000'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('testtest'); + LegacyUnit.equal( + editor.getContent(), + 'testtest
' + ); + + editor.setContent('test 123
'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + }); + + suite.test('Formatting commands (alignInline)', function (editor) { + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('JustifyLeft'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + LegacyUnit.equal(true, editor.queryCommandState('JustifyLeft'), 'should have JustifyLeft state true'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('JustifyCenter'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + LegacyUnit.equal(true, editor.queryCommandState('JustifyCenter'), 'should have JustifyCenter state true'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('JustifyRight'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + LegacyUnit.equal(true, editor.queryCommandState('JustifyRight'), 'should have JustifyRight state true'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('JustifyFull'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + LegacyUnit.equal(true, editor.queryCommandState('JustifyFull'), 'should have JustifyFull state true'); + + editor.setContent('
');
+ editor.selection.select(editor.dom.select('img')[0]);
+ editor.execCommand('JustifyLeft');
+ LegacyUnit.equal(editor.getContent(), '
');
+ editor.selection.select(editor.dom.select('img')[0]);
+ editor.execCommand('JustifyCenter');
+ LegacyUnit.equal(
+ editor.getContent(),
+ '
');
+ editor.selection.select(editor.dom.select('img')[0]);
+ editor.execCommand('JustifyRight');
+ LegacyUnit.equal(editor.getContent(), '
test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('mceBlockQuote'); + LegacyUnit.equal(editor.getContent().replace(/\s+/g, ''), ''); + + editor.setContent('test123
test 123
test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('mceBlockQuote'); + LegacyUnit.equal(editor.getContent().replace(/\s+/g, ''), ''); + }); + + suite.test('FormatBlock', function (editor) { + editor.setContent('test123
test123
test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('FormatBlock', false, 'h1'); + LegacyUnit.equal(editor.getContent(), 'test 123'); + }); + + suite.test('mceInsertLink (relative)', function (editor) { + editor.setContent('test 123'); + editor.execCommand('SelectAll'); + editor.execCommand('mceInsertLink', false, 'test'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link absolute)', function (editor) { + editor.setContent('
test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('mceInsertLink', false, 'http://www.site.com'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link encoded)', function (editor) { + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('mceInsertLink', false, '"&<>'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link encoded and with class)', function (editor) { + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('mceInsertLink', false, { href: '"&<>', class: 'test' }); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link with space)', function (editor) { + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('mceInsertLink', false, { href: 'foo bar' }); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link floated img)', function (editor) { + editor.setContent('ab
'); + + rng = editor.dom.createRng(); + rng.setStart(editor.getBody().firstChild.lastChild, 0); + rng.setEnd(editor.getBody().firstChild.lastChild, 1); + editor.selection.setRng(rng); + + editor.execCommand('mceInsertLink', false, 'link'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link text inside text)', function (editor) { + editor.setContent(''); + LegacyUnit.setSelection(editor, 'em', 1, 'em', 2); + + editor.execCommand('mceInsertLink', false, 'link'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link around existing links)', function (editor) { + editor.setContent(''); + editor.execCommand('SelectAll'); + + editor.execCommand('mceInsertLink', false, 'link'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link around existing links with different attrs)', function (editor) { + editor.setContent(''); + editor.execCommand('SelectAll'); + + editor.execCommand('mceInsertLink', false, 'link'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink (link around existing complex contents with links)', function (editor) { + editor.setContent( + '' + ); + editor.execCommand('SelectAll'); + + editor.execCommand('mceInsertLink', false, 'link'); + LegacyUnit.equal( + editor.getContent(), + '' + ); + }); + + suite.test('mceInsertLink (link text inside link)', function (editor) { + editor.setContent(''); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.execCommand('SelectAll'); + + editor.execCommand('mceInsertLink', false, 'link'); + LegacyUnit.equal(editor.getContent(), ''); + }); + + suite.test('mceInsertLink bug #7331', function (editor) { + editor.setContent('| A |
| B |
| A |
| B |
test 123
'); + }); + + suite.test('unlink - unselected a[href] with childNodes', function (editor) { + editor.setContent(''); + LegacyUnit.setSelection(editor, 'em', 0); + editor.execCommand('unlink'); + LegacyUnit.equal(editor.getContent(), 'test
'); + }); + + suite.test('subscript/superscript', function (editor) { + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('subscript'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('superscript'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('subscript'); + editor.execCommand('subscript'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('superscript'); + editor.execCommand('superscript'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + }); + + suite.test('indent/outdent', function (editor) { + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('Indent'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('Indent'); + editor.execCommand('Indent'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('Indent'); + editor.execCommand('Indent'); + editor.execCommand('Outdent'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + + editor.setContent('test 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('Outdent'); + LegacyUnit.equal(editor.getContent(), 'test 123
'); + }); + + suite.test('indent/outdent table always uses margin', function (editor) { + editor.setContent('| test |
| test |
| test |
| test |
| test |
| test |
| test |
| test |
test 123 123 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('RemoveFormat'); + LegacyUnit.equal(editor.getContent(), 'test 123 123 123
'); + + editor.setContent('test 123 123 123
'); + editor.execCommand('SelectAll'); + editor.execCommand('RemoveFormat'); + LegacyUnit.equal(editor.getContent(), 'test 123 123 123
'); + + editor.setContent('testtest 123123 123
'); + editor.selection.select(editor.dom.get('x')); + editor.execCommand('RemoveFormat'); + LegacyUnit.equal(editor.getContent(), 'testtest 123123 123
'); + + editor.setContent( + 'dfn tag code tag samp tag ' +
+ ' kbd tag var tag cite tag mark tag q tag
dfn tag code tag samp tag kbd tag var tag cite tag mark tag q tag
'); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + add_unload_trigger: false, + disable_nodechange: true, + indent: false, + entities: 'raw', + convert_urls: false, + 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,padding-left,text-align,display' + }, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/InlineEditorRemoveTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/InlineEditorRemoveTest.ts new file mode 100644 index 000000000..3a3daee34 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/InlineEditorRemoveTest.ts @@ -0,0 +1,40 @@ +import { Pipeline, Logger, Chain, UiFinder } from '@ephox/agar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { Editor as McEditor, ApiChains } from '@ephox/mcagar'; +import { Body } from '@ephox/sugar'; + +UnitTest.asynctest('browser.tinymce.core.InlineEditorRemoveTest', (success, failure) => { + Theme(); + + const settings = { + inline: true, + skin_url: '/project/js/tinymce/skins/lightgray' + }; + + const cAssertBogusNotExist = Chain.async((val, next, die) => { + UiFinder.findIn(Body.body(), '[data-mce-bogus]').fold( + () => { + next(val); + }, + () => { + die('Should not be any data-mce-bogus tags present'); + } + ); + }); + + const cRemoveEditor = Chain.op((editor: any) => editor.remove()); + + Pipeline.async({}, [ + Logger.t('Removing inline editor should remove all data-mce-bogus tags', Chain.asStep({}, [ + McEditor.cFromHtml('', settings), + ApiChains.cSetRawContent('b
b
'), + cRemoveEditor, + cAssertBogusNotExist, + McEditor.cRemove, + ]), + ) + ], function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/InlineEditorSaveTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/InlineEditorSaveTest.ts new file mode 100644 index 000000000..02026d8d7 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/InlineEditorSaveTest.ts @@ -0,0 +1,41 @@ +import { Pipeline, Logger, Chain, UiFinder } from '@ephox/agar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import { Editor as McEditor, ApiChains } from '@ephox/mcagar'; +import { Body } from '@ephox/sugar'; +import { Editor } from 'tinymce/core/api/Editor'; + +UnitTest.asynctest('browser.tinymce.core.InlineEditorSaveTest', (success, failure) => { + Theme(); + + const settings = { + inline: true, + skin_url: '/project/js/tinymce/skins/lightgray' + }; + + const cAssertBogusExist = Chain.async((val, next, die) => { + UiFinder.findIn(Body.body(), '[data-mce-bogus]').fold( + () => { + die('Should be data-mce-bogus tags present'); + }, + () => { + next(val); + } + ); + }); + + const cSaveEditor = Chain.op((editor: Editor) => editor.save()); + + Pipeline.async({}, [ + Logger.t('Saving inline editor should not remove data-mce-bogus tags', Chain.asStep({}, [ + McEditor.cFromHtml('', settings), + ApiChains.cSetRawContent('b
b
'), + cSaveEditor, + cAssertBogusExist, + McEditor.cRemove, + ]), + ) + ], function () { + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/JqueryIntegrationTest.disabled b/tools-ng/tinymce/editor/src/core/test/ts/browser/JqueryIntegrationTest.disabled new file mode 100644 index 000000000..751552e71 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/JqueryIntegrationTest.disabled @@ -0,0 +1,142 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit } from '@ephox/mcagar'; +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import ScriptLoader from 'tinymce/core/api/dom/ScriptLoader'; +import { Editor } from 'tinymce/core/api/Editor'; +import EditorManager from 'tinymce/core/api/EditorManager'; +import JqueryIntegration from 'tinymce/core/JqueryIntegration'; +import PluginManager from 'tinymce/core/api/PluginManager'; +import ViewBlock from '../module/test/ViewBlock'; +import ThemeManager from 'tinymce/core/api/ThemeManager'; +import Delay from 'tinymce/core/api/util/Delay'; +import Tools from 'tinymce/core/api/util/Tools'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +declare const window: any; + +UnitTest.asynctest('browser.tinymce.core.JqueryIntegrationTest', function() { + var success = arguments[arguments.length - 2]; + var failure = arguments[arguments.length - 1]; + var suite = LegacyUnit.createSuite(); + var viewBlock = ViewBlock(); + var $; + + Theme(); + + var setup = function () { + viewBlock.attach(); + viewBlock.update( + '' + + '' + + '' + ); + }; + + var loadJquery = function (done) { + var script = document.createElement('script'); + script.src = '/project/js/tinymce/skins/lightgray/node_modules/jquery/dist/jquery.min.js'; + script.onload = function () { + script.parentNode.removeChild(script); + $ = window.jQuery.noConflict(true); + JqueryIntegration({ tinymce: EditorManager, jQuery: $ }); + done(); + }; + document.body.appendChild(script); + }; + + suite.asyncTest('Setup editors', function (_, done) { + $(function () { + $('#elm1,#elm2').tinymce({ + skin_url: '/project/js/tinymce/skins/lightgray', + init_instance_callback: function () { + var ed1 = EditorManager.get('elm1'), ed2 = EditorManager.get('elm2'); + + // When both editors are initialized + if (ed1 && ed1.initialized && ed2 && ed2.initialized) { + done(); + } + } + }); + }); + }); + + suite.test("Get editor instance", function () { + LegacyUnit.equal($('#elm1').tinymce().id, 'elm1'); + LegacyUnit.equal($('#elm2').tinymce().id, 'elm2'); + LegacyUnit.equal($('#elm3').tinymce(), null); + }); + + suite.test("Get contents using jQuery", function () { + EditorManager.get('elm1').setContent('Editor 1
'); + + LegacyUnit.equal($('#elm1').html(), 'Editor 1
'); + LegacyUnit.equal($('#elm1').val(), 'Editor 1
'); + LegacyUnit.equal($('#elm1').attr('value'), 'Editor 1
'); + LegacyUnit.equal($('#elm1').text(), 'Editor 1'); + }); + + suite.test("Set contents using jQuery", function () { + $('#elm1').html('Test 1'); + LegacyUnit.equal($('#elm1').html(), 'Test 1
'); + + $('#elm1').val('Test 2'); + LegacyUnit.equal($('#elm1').html(), 'Test 2
'); + + $('#elm1').text('Test 3'); + LegacyUnit.equal($('#elm1').html(), 'Test 3
'); + + $('#elm1').attr('value', 'Test 4'); + LegacyUnit.equal($('#elm1').html(), 'Test 4
'); + }); + + suite.test("append/prepend contents using jQuery", function () { + EditorManager.get('elm1').setContent('Editor 1
'); + + $('#elm1').append('Test 1
'); + LegacyUnit.equal($('#elm1').html(), 'Editor 1
\nTest 1
'); + + $('#elm1').prepend('Test 2
'); + LegacyUnit.equal($('#elm1').html(), 'Test 2
\nEditor 1
\nTest 1
'); + }); + + suite.test("Find using :tinymce selector", function () { + LegacyUnit.equal($('textarea:tinymce').length, 2); + }); + + suite.test("Set contents using :tinymce selector", function () { + $('textarea:tinymce').val('Test 1'); + LegacyUnit.equal($('#elm1').val(), 'Test 1
'); + LegacyUnit.equal($('#elm2').val(), 'Test 1
'); + LegacyUnit.equal($('#elm3').val(), 'Textarea'); + }); + + suite.test("Get contents using :tinymce selector", function () { + $('textarea:tinymce').val('Test get'); + LegacyUnit.equal($('textarea:tinymce').val(), 'Test get
'); + }); + + suite.test("applyPatch is only called once", function () { + var options = {}, oldValFn; + + $('#elm1').tinymce(options); + + oldValFn = $.fn.val = function () { + // no-op + }; + + $('#elm2').tinymce(options); + + LegacyUnit.equal($.fn.val, oldValFn); + }); + + loadJquery(function () { + setup(); + Pipeline.async({}, suite.toSteps({}), function () { + EditorManager.remove(); + viewBlock.detach(); + success(); + }, failure); + }); +}); + diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/MiscCommandsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/MiscCommandsTest.ts new file mode 100644 index 000000000..e39a7861c --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/MiscCommandsTest.ts @@ -0,0 +1,100 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import Env from 'tinymce/core/api/Env'; +import HtmlUtils from '../module/test/HtmlUtils'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.MiscCommandsTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + const normalizeRng = function (rng) { + if (rng.startContainer.nodeType === 3) { + if (rng.startOffset === 0) { + rng.setStartBefore(rng.startContainer); + } else if (rng.startOffset >= rng.startContainer.nodeValue.length - 1) { + rng.setStartAfter(rng.startContainer); + } + } + + if (rng.endContainer.nodeType === 3) { + if (rng.endOffset === 0) { + rng.setEndBefore(rng.endContainer); + } else if (rng.endOffset >= rng.endContainer.nodeValue.length - 1) { + rng.setEndAfter(rng.endContainer); + } + } + + return rng; + }; + + const ok = function (value, label?) { + return LegacyUnit.equal(value, true, label); + }; + + suite.test('InsertHorizontalRule', function (editor) { + let rng; + + editor.setContent('123
'); + rng = editor.dom.createRng(); + rng.setStart(editor.dom.select('p')[0].firstChild, 1); + rng.setEnd(editor.dom.select('p')[0].firstChild, 2); + editor.selection.setRng(rng); + editor.execCommand('InsertHorizontalRule'); + LegacyUnit.equal(editor.getContent(), '1
3
'); + rng = normalizeRng(editor.selection.getRng(true)); + ok(rng.collapsed); + LegacyUnit.equalDom(rng.startContainer, editor.getBody().lastChild); + LegacyUnit.equal(rng.startContainer.nodeName, 'P'); + LegacyUnit.equal(rng.startOffset, 0); + LegacyUnit.equal(rng.endContainer.nodeName, 'P'); + LegacyUnit.equal(rng.endOffset, 0); + }); + + if (Env.ceFalse) { + suite.test('SelectAll', function (editor) { + editor.setContent('a
c
'); + LegacyUnit.setSelection(editor, 'div div', 0); + editor.execCommand('SelectAll'); + LegacyUnit.equal(editor.selection.getStart().nodeName, 'DIV'); + LegacyUnit.equal(editor.selection.getEnd().nodeName, 'DIV'); + LegacyUnit.equal(editor.selection.isCollapsed(), false); + }); + } + + suite.test('InsertLineBreak', function (editor) { + editor.setContent('123
'); + LegacyUnit.setSelection(editor, 'p', 2); + editor.execCommand('InsertLineBreak'); + LegacyUnit.equal(editor.getContent(), '12
3
123
'); + LegacyUnit.setSelection(editor, 'p', 0); + editor.execCommand('InsertLineBreak'); + LegacyUnit.equal(editor.getContent(), '
123
123
'); + LegacyUnit.setSelection(editor, 'p', 3); + editor.execCommand('InsertLineBreak'); + LegacyUnit.equal(HtmlUtils.cleanHtml(editor.getBody().innerHTML), (Env.ie && Env.ie < 11) ? '123
123
abc'); + + LegacyUnit.setSelection(editor, 'pre', 1); + arrow(editor); + LegacyUnit.equal(editor.getContent(), '
abc'); + LegacyUnit.equal(editor.selection.getNode().nodeName, 'PRE'); + + LegacyUnit.setSelection(editor, 'pre', offset); + arrow(editor); + LegacyUnit.equal(editor.getContent(), expectedContent); + LegacyUnit.equal(editor.selection.getNode().nodeName, 'P'); + }; + }; + + const ok = function (a, label) { + LegacyUnit.equal(a, true, label); + }; + + const leftArrow = pressKey(VK.LEFT); + const rightArrow = pressKey(VK.RIGHT); + const upArrow = pressKey(VK.UP); + const downArrow = pressKey(VK.DOWN); + + suite.test('left/right over cE=false inline', function (editor) { + editor.focus(); + editor.setContent('1'); + editor.selection.select(editor.$('span')[0]); + + leftArrow(editor); + LegacyUnit.equal(editor.getContent(), '
1
'); + LegacyUnit.equal(CaretContainer.isCaretContainerInline(editor.selection.getRng().startContainer), true); + LegacyUnit.equalDom(editor.selection.getRng().startContainer, editor.$('p')[0].firstChild); + + rightArrow(editor); + LegacyUnit.equal(editor.getContent(), '1
'); + LegacyUnit.equalDom(editor.selection.getNode(), editor.$('span')[0]); + + rightArrow(editor); + LegacyUnit.equal(editor.getContent(), '1
'); + LegacyUnit.equal(CaretContainer.isCaretContainerInline(editor.selection.getRng().startContainer), true); + LegacyUnit.equalDom(editor.selection.getRng().startContainer, editor.$('p')[0].lastChild); + }); + + suite.test('left/right over cE=false block', function (editor) { + editor.setContent('1
'); + editor.selection.select(editor.$('p[contenteditable=false]')[0]); + + leftArrow(editor); + LegacyUnit.equal(editor.getContent(), '1
'); + LegacyUnit.equal(CaretContainer.isCaretContainerBlock(editor.selection.getRng().startContainer), true); + + rightArrow(editor); + LegacyUnit.equal(editor.getContent(), '1
'); + LegacyUnit.equalDom(editor.selection.getNode(), editor.$('p[contenteditable=false]')[0]); + + rightArrow(editor); + LegacyUnit.equal(editor.getContent(), '1
'); + LegacyUnit.equal(CaretContainer.isCaretContainerBlock(editor.selection.getRng().startContainer), true); + }); + + suite.test('left before cE=false block and type', function (editor) { + editor.setContent('1
'); + editor.selection.select(editor.$('p')[0]); + + leftArrow(editor); + KeyUtils.type(editor, 'a'); + LegacyUnit.equal(editor.getContent(), 'a
1
'); + LegacyUnit.equal(CaretContainer.isCaretContainerBlock(editor.selection.getRng().startContainer.parentNode), false); + }); + + suite.test('right after cE=false block and type', function (editor) { + editor.setContent('1
'); + editor.selection.select(editor.$('p[contenteditable=false]')[0]); + + rightArrow(editor); + KeyUtils.type(editor, 'a'); + LegacyUnit.equal(editor.getContent(), '1
a
'); + LegacyUnit.equal(CaretContainer.isCaretContainerBlock(editor.selection.getRng().startContainer.parentNode), false); + }); + + suite.test('up from P to inline cE=false', function (editor) { + editor.setContent('a1
abc
'); + LegacyUnit.setSelection(editor, 'p:last', 3); + + upArrow(editor); + LegacyUnit.equal(CaretContainer.isCaretContainerInline(editor.$('p:first')[0].lastChild), true); + }); + + suite.test('down from P to inline cE=false', function (editor) { + editor.setContent('abc
a1
'); + LegacyUnit.setSelection(editor, 'p:first', 3); + + downArrow(editor); + LegacyUnit.equal(CaretContainer.isCaretContainerInline(editor.$('p:last')[0].lastChild), true); + }); + + suite.test('exit pre block (up)', exitPreTest(upArrow, 0, '\u00a0
abc')); + suite.test('exit pre block (left)', exitPreTest(leftArrow, 0, '
\u00a0
abc')); + suite.test('exit pre block (down)', exitPreTest(downArrow, 3, '
abc
\u00a0
')); + suite.test('exit pre block (right)', exitPreTest(rightArrow, 3, 'abc
\u00a0
')); + + suite.test('click on link in cE=false', function (editor) { + editor.setContent(''); + const evt = editor.fire('click', { target: editor.$('strong')[0] }); + + LegacyUnit.equal(evt.isDefaultPrevented(), true); + }); + + suite.test('click next to cE=false block', function (editor) { + editor.setContent( + '| 1 | ' + + '2 | ' +
+ '
| 1 | 2 |
1
2
'); + + const rng = document.createRange(); + rng.setStartBefore(editor.dom.select('p[contenteditable=false]')[1]); + rng.setEndBefore(editor.dom.select('p[contenteditable=false]')[1]); + + editor.selection.setRng(rng, false); + LegacyUnit.equal(editor.selection.getNode().getAttribute('data-mce-caret'), 'after'); + }); + + suite.test('set range after ce=false element but lean forwards', function (editor) { + editor.setContent('1
2
'); + + const rng = document.createRange(); + rng.setStartBefore(editor.dom.select('p[contenteditable=false]')[1]); + rng.setEndBefore(editor.dom.select('p[contenteditable=false]')[1]); + + editor.selection.setRng(rng, true); + LegacyUnit.equal(editor.selection.getNode().getAttribute('data-mce-caret'), 'before'); + }); + + suite.test('showCaret at TD', function (editor) { + let rng; + + editor.setContent('| x |
| x |
|---|
a
b
'); + + rng = editor._selectionOverrides.showCaret(1, editor.dom.select('p[contenteditable=false]')[0], true); + LegacyUnit.equal(true, rng !== null, 'Should return a range'); + editor._selectionOverrides.hideFakeCaret(); + + rng = editor._selectionOverrides.showCaret(1, editor.dom.select('p[contenteditable=false]')[1], false); + LegacyUnit.equal(true, rng === null, 'Should not return a range excluded by ShowCaret event'); + editor._selectionOverrides.hideFakeCaret(); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + selector: 'textarea', + add_unload_trigger: false, + disable_nodechange: true, + entities: 'raw', + indent: false, + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/ShortcutsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/ShortcutsTest.ts new file mode 100644 index 000000000..6ca61bfca --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/ShortcutsTest.ts @@ -0,0 +1,92 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import Env from 'tinymce/core/api/Env'; +import Tools from 'tinymce/core/api/util/Tools'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.ShortcutsTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + suite.test('Shortcuts formats', function (editor) { + const assertShortcut = function (shortcut, args, assertState) { + let called = false; + + editor.shortcuts.add(shortcut, '', function () { + called = true; + }); + + args = Tools.extend({ + ctrlKey: false, + altKey: false, + shiftKey: false, + metaKey: false + }, args); + + editor.fire('keydown', args); + + if (assertState) { + LegacyUnit.equal(called, true, 'Shortcut wasn\'t called: ' + shortcut); + } else { + LegacyUnit.equal(called, false, 'Shortcut was called when it shouldn\'t have been: ' + shortcut); + } + }; + + assertShortcut('ctrl+d', { ctrlKey: true, keyCode: 68 }, true); + assertShortcut('ctrl+d', { altKey: true, keyCode: 68 }, false); + + if (Env.mac) { + assertShortcut('meta+d', { metaKey: true, keyCode: 68 }, true); + assertShortcut('access+d', { ctrlKey: true, altKey: true, keyCode: 68 }, true); + assertShortcut('meta+d', { ctrlKey: true, keyCode: 68 }, false); + assertShortcut('access+d', { shiftKey: true, altKey: true, keyCode: 68 }, false); + } else { + assertShortcut('meta+d', { ctrlKey: true, keyCode: 68 }, true); + assertShortcut('access+d', { shiftKey: true, altKey: true, keyCode: 68 }, true); + assertShortcut('meta+d', { metaKey: true, keyCode: 68 }, false); + assertShortcut('access+d', { ctrlKey: true, altKey: true, keyCode: 68 }, false); + } + + assertShortcut('ctrl+shift+d', { ctrlKey: true, shiftKey: true, keyCode: 68 }, true); + assertShortcut('ctrl+shift+alt+d', { ctrlKey: true, shiftKey: true, altKey: true, keyCode: 68 }, true); + assertShortcut('ctrl+221', { ctrlKey: true, keyCode: 221 }, true); + }); + + suite.test('Remove', function (editor) { + let called = false, eventArgs; + + eventArgs = { + ctrlKey: true, + keyCode: 68, + altKey: false, + shiftKey: false, + metaKey: false + }; + + editor.shortcuts.add('ctrl+d', '', function () { + called = true; + }); + + editor.fire('keydown', eventArgs); + LegacyUnit.equal(called, true, 'Shortcut wasn\'t called when it should have been.'); + + called = false; + editor.shortcuts.remove('ctrl+d'); + editor.fire('keydown', eventArgs); + LegacyUnit.equal(called, false, 'Shortcut was called when it shouldn\'t.'); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + add_unload_trigger: false, + disable_nodechange: true, + indent: false, + entities: 'raw', + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/UndoManagerTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/UndoManagerTest.ts new file mode 100644 index 000000000..e1f04bdb1 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/UndoManagerTest.ts @@ -0,0 +1,512 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import Env from 'tinymce/core/api/Env'; +import HtmlUtils from '../module/test/HtmlUtils'; +import KeyUtils from '../module/test/KeyUtils'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.UndoManager', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + const ok = function (value, label?) { + return LegacyUnit.equal(value, true, label); + }; + + suite.test('Initial states', function (editor) { + ok(!editor.undoManager.hasUndo()); + ok(!editor.undoManager.hasRedo()); + ok(!editor.undoManager.typing); + }); + + suite.test('One undo level', function (editor) { + editor.undoManager.clear(); + editor.setContent('test'); + + editor.focus(); + editor.execCommand('SelectAll'); + editor.execCommand('Bold'); + + ok(editor.undoManager.hasUndo()); + ok(!editor.undoManager.hasRedo()); + ok(!editor.undoManager.typing); + }); + + suite.test('Two undo levels', function (editor) { + editor.undoManager.clear(); + editor.setContent('test'); + + editor.execCommand('SelectAll'); + editor.execCommand('Bold'); + editor.execCommand('SelectAll'); + editor.execCommand('Italic'); + + ok(editor.undoManager.hasUndo()); + ok(!editor.undoManager.hasRedo()); + ok(!editor.undoManager.typing); + }); + + suite.test('No undo levels and one redo', function (editor) { + editor.undoManager.clear(); + editor.setContent('test'); + + editor.execCommand('SelectAll'); + editor.execCommand('Bold'); + editor.undoManager.undo(); + + ok(!editor.undoManager.hasUndo()); + ok(editor.undoManager.hasRedo()); + ok(!editor.undoManager.typing); + }); + + suite.test('One undo levels and one redo', function (editor) { + editor.undoManager.clear(); + editor.setContent('test'); + + editor.execCommand('SelectAll'); + editor.execCommand('Bold'); + editor.execCommand('SelectAll'); + editor.execCommand('Italic'); + editor.undoManager.undo(); + + ok(editor.undoManager.hasUndo()); + ok(editor.undoManager.hasRedo()); + ok(!editor.undoManager.typing); + }); + + suite.test('Typing state', function (editor) { + let selectAllFlags; + + editor.undoManager.clear(); + editor.setContent('test'); + + ok(!editor.undoManager.typing); + + editor.dom.fire(editor.getBody(), 'keydown', { keyCode: 65 }); + ok(editor.undoManager.typing); + + editor.dom.fire(editor.getBody(), 'keydown', { keyCode: 13 }); + ok(!editor.undoManager.typing); + + selectAllFlags = { keyCode: 65, ctrlKey: false, altKey: false, shiftKey: false }; + + if (Env.mac) { + selectAllFlags.metaKey = true; + } else { + selectAllFlags.ctrlKey = true; + } + + editor.dom.fire(editor.getBody(), 'keydown', selectAllFlags); + ok(!editor.undoManager.typing); + }); + + suite.test('Undo and add new level', function (editor) { + editor.undoManager.clear(); + editor.setContent('test'); + + editor.execCommand('SelectAll'); + editor.execCommand('Bold'); + editor.undoManager.undo(); + editor.execCommand('SelectAll'); + editor.execCommand('Italic'); + + ok(editor.undoManager.hasUndo()); + ok(!editor.undoManager.hasRedo()); + ok(!editor.undoManager.typing); + }); + + suite.test('Events', function (editor) { + let add, undo, redo; + + editor.undoManager.clear(); + editor.setContent('test'); + + editor.on('AddUndo', function (e) { + add = e.level; + }); + + editor.on('Undo', function (e) { + undo = e.level; + }); + + editor.on('Redo', function (e) { + redo = e.level; + }); + + editor.execCommand('SelectAll'); + editor.execCommand('Bold'); + ok(!!add.content); + ok(!!add.bookmark); + + editor.undoManager.undo(); + ok(!!undo.content); + ok(!!undo.bookmark); + + editor.undoManager.redo(); + ok(!!redo.content); + ok(!!redo.bookmark); + }); + + suite.test('No undo/redo cmds on Undo/Redo shortcut', function (editor) { + let evt; + const commands = []; + let added = false; + + editor.undoManager.clear(); + editor.setContent('test'); + + editor.on('BeforeExecCommand', function (e) { + commands.push(e.command); + }); + + editor.on('BeforeAddUndo', function () { + added = true; + }); + + evt = { + keyCode: 90, + metaKey: Env.mac, + ctrlKey: !Env.mac, + shiftKey: false, + altKey: false + }; + + editor.dom.fire(editor.getBody(), 'keydown', evt); + editor.dom.fire(editor.getBody(), 'keypress', evt); + editor.dom.fire(editor.getBody(), 'keyup', evt); + + LegacyUnit.strictEqual(added, false); + LegacyUnit.deepEqual(commands, ['Undo']); + }); + + suite.test('Transact', function (editor) { + let count = 0, level; + + editor.undoManager.clear(); + + editor.on('BeforeAddUndo', function () { + count++; + }); + + level = editor.undoManager.transact(function () { + editor.undoManager.add(); + editor.undoManager.add(); + }); + + LegacyUnit.equal(count, 1); + LegacyUnit.equal(level !== null, true); + }); + + suite.test('Transact no change', function (editor) { + editor.undoManager.add(); + + const level = editor.undoManager.transact(function () { + }); + + LegacyUnit.equal(level, null); + }); + + suite.test('Transact with change', function (editor) { + editor.undoManager.add(); + + const level = editor.undoManager.transact(function () { + editor.setContent('x'); + }); + + LegacyUnit.equal(level !== null, true); + }); + + suite.test('Transact nested', function (editor) { + let count = 0; + + editor.undoManager.clear(); + + editor.on('BeforeAddUndo', function () { + count++; + }); + + editor.undoManager.transact(function () { + editor.undoManager.add(); + + editor.undoManager.transact(function () { + editor.undoManager.add(); + }); + }); + + LegacyUnit.equal(count, 1); + }); + + suite.test('Transact exception', function (editor) { + let count = 0; + + editor.undoManager.clear(); + + editor.on('BeforeAddUndo', function () { + count++; + }); + + try { + editor.undoManager.transact(function () { + throw new Error('Test'); + }); + + LegacyUnit.equal(true, false, 'Should never get here!'); + } catch (ex) { + LegacyUnit.equal(ex.message, 'Test'); + } + + editor.undoManager.add(); + + LegacyUnit.equal(count, 1); + }); + + suite.test('Extra with changes', function (editor) { + let data; + + editor.undoManager.clear(); + editor.setContent('abc
'); + LegacyUnit.setSelection(editor, 'p', 0); + editor.undoManager.add(); + + editor.undoManager.extra(function () { + LegacyUnit.setSelection(editor, 'p', 1, 'p', 2); + editor.insertContent('1'); + }, function () { + LegacyUnit.setSelection(editor, 'p', 1, 'p', 2); + editor.insertContent('2'); + }); + + data = editor.undoManager.data; + LegacyUnit.equal(data.length, 3); + LegacyUnit.equal(data[0].content, 'abc
'); + LegacyUnit.deepEqual(data[0].bookmark, { start: [0, 0, 0] }); + LegacyUnit.deepEqual(data[0].beforeBookmark, { start: [0, 0, 0] }); + LegacyUnit.equal(data[1].content, 'a1c
'); + LegacyUnit.deepEqual(data[1].bookmark, { start: [2, 0, 0] }); + LegacyUnit.deepEqual(data[1].beforeBookmark, { start: [2, 0, 0] }); + LegacyUnit.equal(data[2].content, 'a2c
'); + LegacyUnit.deepEqual(data[2].bookmark, { start: [2, 0, 0] }); + LegacyUnit.deepEqual(data[1].beforeBookmark, data[2].bookmark); + }); + + suite.test('Exclude internal elements', function (editor) { + let count = 0, lastLevel; + + editor.undoManager.clear(); + LegacyUnit.equal(count, 0); + + editor.on('AddUndo', function () { + count++; + }); + + editor.on('BeforeAddUndo', function (e) { + lastLevel = e.level; + }); + + editor.getBody().innerHTML = ( + 'test' + + '| x |
| x |
| x |
| x |
some text
'); + LegacyUnit.setSelection(editor, 'p', 4, 'p', 9); + KeyUtils.type(editor, '\b'); + + LegacyUnit.equal(HtmlUtils.cleanHtml(lastLevel.content), 'some text
'); + editor.fire('blur'); + LegacyUnit.equal(HtmlUtils.cleanHtml(lastLevel.content), 'some
'); + + editor.execCommand('FormatBlock', false, 'h1'); + editor.undoManager.undo(); + LegacyUnit.equal(editor.getContent(), 'some
'); + }); + + suite.test('BeforeAddUndo event', function (editor) { + let lastEvt, addUndoEvt; + + const blockEvent = function (e) { + e.preventDefault(); + }; + + editor.on('BeforeAddUndo', function (e) { + lastEvt = e; + }); + + editor.undoManager.clear(); + editor.setContent('a
'); + editor.undoManager.add(); + + LegacyUnit.equal(lastEvt.lastLevel, undefined); + LegacyUnit.equal(HtmlUtils.cleanHtml(lastEvt.level.content), 'a
'); + + editor.setContent('b
'); + editor.undoManager.add(); + + LegacyUnit.equal(HtmlUtils.cleanHtml(lastEvt.lastLevel.content), 'a
'); + LegacyUnit.equal(HtmlUtils.cleanHtml(lastEvt.level.content), 'b
'); + + editor.on('BeforeAddUndo', blockEvent); + + editor.on('AddUndo', function (e) { + addUndoEvt = e; + }); + + editor.setContent('c
'); + editor.undoManager.add(null, { data: 1 }); + + LegacyUnit.equal(HtmlUtils.cleanHtml(lastEvt.lastLevel.content), 'b
'); + LegacyUnit.equal(HtmlUtils.cleanHtml(lastEvt.level.content), 'c
'); + LegacyUnit.equal(lastEvt.originalEvent.data, 1); + ok(!addUndoEvt, 'Event level produced when it should be blocked'); + + editor.off('BeforeAddUndo', blockEvent); + }); + + suite.test('Dirty state type letter', function (editor) { + editor.undoManager.clear(); + editor.setDirty(false); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 1); + + ok(!editor.isDirty(), 'Dirty state should be false'); + KeyUtils.type(editor, 'b'); + LegacyUnit.equal(editor.getContent(), 'ab
'); + ok(editor.isDirty(), 'Dirty state should be true'); + }); + + suite.test('Dirty state type shift+letter', function (editor) { + editor.undoManager.clear(); + editor.setDirty(false); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 1); + + ok(!editor.isDirty(), 'Dirty state should be false'); + KeyUtils.type(editor, { keyCode: 65, charCode: 66, shiftKey: true }); + LegacyUnit.equal(editor.getContent(), 'aB
'); + ok(editor.isDirty(), 'Dirty state should be true'); + }); + + suite.test('Dirty state type AltGr+letter', function (editor) { + editor.undoManager.clear(); + editor.setDirty(false); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 1); + + ok(!editor.isDirty(), 'Dirty state should be false'); + KeyUtils.type(editor, { keyCode: 65, charCode: 66, ctrlKey: true, altKey: true }); + LegacyUnit.equal(editor.getContent(), 'aB
'); + ok(editor.isDirty(), 'Dirty state should be true'); + }); + + suite.test('ExecCommand while typing should produce undo level', function (editor) { + editor.undoManager.clear(); + editor.setDirty(false); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 1); + + LegacyUnit.equal(editor.undoManager.typing, false); + KeyUtils.type(editor, { keyCode: 66, charCode: 66 }); + LegacyUnit.equal(editor.undoManager.typing, true); + LegacyUnit.equal(editor.getContent(), 'aB
'); + editor.execCommand('mceInsertContent', false, 'C'); + LegacyUnit.equal(editor.undoManager.typing, false); + LegacyUnit.equal(editor.undoManager.data.length, 3); + LegacyUnit.equal(editor.undoManager.data[0].content, 'a
'); + LegacyUnit.equal(editor.undoManager.data[1].content, 'aB
'); + LegacyUnit.equal(editor.undoManager.data[2].content, 'aBC
'); + }); + + suite.test('transact while typing should produce undo level', function (editor) { + editor.undoManager.clear(); + editor.setDirty(false); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 1); + + LegacyUnit.equal(editor.undoManager.typing, false); + KeyUtils.type(editor, { keyCode: 66, charCode: 66 }); + LegacyUnit.equal(editor.undoManager.typing, true); + LegacyUnit.equal(editor.getContent(), 'aB
'); + editor.undoManager.transact(function () { + editor.getBody().firstChild.firstChild.data = 'aBC'; + }); + LegacyUnit.equal(editor.undoManager.typing, false); + LegacyUnit.equal(editor.undoManager.data.length, 3); + LegacyUnit.equal(editor.undoManager.data[0].content, 'a
'); + LegacyUnit.equal(editor.undoManager.data[1].content, 'aB
'); + LegacyUnit.equal(editor.undoManager.data[2].content, 'aBC
'); + }); + + suite.test('ignore does a transaction but no levels', function (editor) { + editor.undoManager.clear(); + editor.setDirty(false); + editor.setContent('a
'); + LegacyUnit.setSelection(editor, 'p', 0, 'p', 1); + editor.undoManager.typing = true; + + editor.undoManager.ignore(function () { + editor.execCommand('bold'); + editor.execCommand('italic'); + }); + + LegacyUnit.equal(editor.undoManager.typing, true); + LegacyUnit.equal(editor.undoManager.data.length, 0); + LegacyUnit.equal(editor.getContent(), 'a
'); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + add_unload_trigger: false, + disable_nodechange: true, + indent: false, + entities: 'raw', + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/WindowManagerTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/WindowManagerTest.ts new file mode 100644 index 000000000..30fd713a0 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/WindowManagerTest.ts @@ -0,0 +1,43 @@ +import { Pipeline } from '@ephox/agar'; +import { LegacyUnit, TinyLoader } from '@ephox/mcagar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.WindowManagerTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + + Theme(); + + suite.test('OpenWindow/CloseWindow events', function (editor) { + let openWindowArgs, closeWindowArgs; + + editor.on('CloseWindow', function (e) { + closeWindowArgs = e; + }); + + editor.on('OpenWindow', function (e) { + openWindowArgs = e; + e.win.close(); + }); + + editor.windowManager.alert('test'); + + LegacyUnit.equal(openWindowArgs.type, 'openwindow'); + LegacyUnit.equal(closeWindowArgs.type, 'closewindow'); + LegacyUnit.equal(editor.windowManager.getWindows().length, 0); + + editor.off('CloseWindow OpenWindow'); + }); + + TinyLoader.setup(function (editor, onSuccess, onFailure) { + Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure); + }, { + add_unload_trigger: false, + disable_nodechange: true, + indent: false, + entities: 'raw', + skin_url: '/project/js/tinymce/skins/lightgray' + }, success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotateTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotateTest.ts new file mode 100644 index 000000000..e009f3267 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotateTest.ts @@ -0,0 +1,226 @@ +import { GeneralSteps, Pipeline, Logger, Assertions, ApproxStructure } from '@ephox/agar'; +import { UnitTest } from '@ephox/bedrock'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import { Editor } from 'tinymce/core/api/Editor'; +import ModernTheme from 'tinymce/themes/modern/Theme'; + +import { sAnnotate, sAssertHtmlContent } from '../../module/test/AnnotationAsserts'; +import { Element } from '@ephox/sugar'; + +UnitTest.asynctest('browser.tinymce.core.annotate.AnnotateTest', (success, failure) => { + ModernTheme(); + + TinyLoader.setup(function (editor: Editor, onSuccess, onFailure) { + const tinyApis = TinyApis(editor); + + // TODO: Consider testing collapse sections. + const sTestWordGrabIfCollapsed = Logger.t( + 'Should word grab with a collapsed selection', + GeneralSteps.sequence([ + // 'This |is| the first paragraph
This is the second.
' + tinyApis.sSetContent('This is the first paragraph here
This is the second.
'), + tinyApis.sSetSelection([ 0, 0 ], 'This is the first p'.length, [ 0, 0 ], 'This is the first p'.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'one-paragraph' }), + sAssertHtmlContent(tinyApis, [ + `This is the first paragraph here
`, + 'This is the second.
FirstThird
'), + tinyApis.sSetSelection([ 1 ], 0, [ 1 ], 0), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'empty-paragraph' }), + sAssertHtmlContent(tinyApis, [ + 'First
', + 'Third
' + ]), + ]) + ); + + const sTestCanAnnotateBeforeTwoNonBreakingSpaces = Logger.t( + 'Should annotate when the cursor is collapsed before two nbsps', + GeneralSteps.sequence([ + tinyApis.sSetContent('Annotation here , please
'), + tinyApis.sSetCursor([ 0, 0 ], 'Annotation here '.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'nbsp-paragraph' }), + Assertions.sAssertStructure( + 'Checking body element', + ApproxStructure.build((s, str, arr) => { + return s.element('body', { + children: [ + s.element('p', { + children: [ + s.text( str.is('Annotation here ') ), + s.element('span', { + classes: [ arr.has('mce-annotation') ], + html: str.is(' ') + }), + s.text( str.is('\u00A0\u00A0, please')) + ] + }) + ] + }); + }), + Element.fromDom(editor.getBody()) + ) + ]) + ); + + const sTestCanAnnotateWithinTwoNonBreakingSpaces = Logger.t( + 'Should annotate when the cursor is collapsed between two nbsps', + GeneralSteps.sequence([ + tinyApis.sSetContent('Annotation here , please
'), + tinyApis.sSetCursor([ 0, 0 ], 'Annotation here '.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'nbsp-paragraph' }), + Assertions.sAssertStructure( + 'Checking body element', + ApproxStructure.build((s, str, arr) => { + return s.element('body', { + children: [ + s.element('p', { + children: [ + s.text( str.is('Annotation here \u00A0') ), + s.element('span', { + classes: [ arr.has('mce-annotation') ], + html: str.is(' ') + }), + s.text( str.is('\u00A0, please')) + ] + }) + ] + }); + }), + Element.fromDom(editor.getBody()) + ) + ]) + ); + + const sTestCanAnnotateAfterTwoNonBreakingSpaces = Logger.t( + 'Should annotate when the cursor is collapsed after two nbsps', + GeneralSteps.sequence([ + tinyApis.sSetContent('Annotation here , please
'), + tinyApis.sSetCursor([ 0, 0 ], 'Annotation here '.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'nbsp-paragraph' }), + Assertions.sAssertStructure( + 'Checking body element', + ApproxStructure.build((s, str, arr) => { + return s.element('body', { + children: [ + s.element('p', { + children: [ + s.text( str.is('Annotation here \u00A0\u00A0') ), + s.element('span', { + classes: [ arr.has('mce-annotation') ], + html: str.is(',') + }), + s.text( str.is(' please')) + ] + }) + ] + }); + }), + Element.fromDom(editor.getBody()) + ) + ]) + ); + + const sTestDoesNotWordGrabIfNotCollapsed = Logger.t( + 'Should not word grab if the selection is not collapsed', + GeneralSteps.sequence([ + // 'This |is| the first paragraph
This is the second.
' + tinyApis.sSetContent('This is the first paragraph
This is the second.
'), + tinyApis.sSetSelection([ 0, 0 ], 'This is the first p'.length, [ 0, 0 ], 'This is the first par'.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'one-paragraph' }), + sAssertHtmlContent(tinyApis, [ + `This is the first paragraph
`, + 'This is the second.
This |is| the first paragraphThis is the second.
' + tinyApis.sSetContent('This is the first paragraph
This is the second.
'), + tinyApis.sSetSelection([ 0, 0 ], 'This '.length, [ 0, 0 ], 'This is'.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'one-paragraph' }), + sAssertHtmlContent(tinyApis, [ + `This is the first paragraph
`, + 'This is the second.
This |is the first paragraphThis is| the second.
' + tinyApis.sSetContent('This is the first paragraph
This is the second.
'), + tinyApis.sSetSelection([ 0, 0 ], 'This '.length, [ 1, 0 ], 'This is'.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'two-paragraphs' }), + sAssertHtmlContent(tinyApis, [ + `This is the first paragraph
`, + `This is the second.
` + ]), + tinyApis.sAssertSelection([ 0 ], 1, [ 1 ], 1) + ]) + ); + + const sTestInThreeParagraphs = Logger.t( + 'Testing over three paragraphs', + GeneralSteps.sequence([ + // 'This |is the first paragraph
This is the second.
This is| the third.
' + tinyApis.sSetContent('This is the first paragraph
This is the second.
This is the third.
'), + tinyApis.sSetSelection([ 0, 0 ], 'This '.length, [ 2, 0 ], 'This is'.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'three-paragraphs' }), + sAssertHtmlContent(tinyApis, [ + `This is the first paragraph
`, + `This is the second.
`, + `This is the third.
` + ]), + tinyApis.sAssertSelection([ 0 ], 1, [ 2 ], 1) + ]) + ); + + Pipeline.async({}, [ + tinyApis.sFocus, + sTestWordGrabIfCollapsed, + sTestDoesNotWordGrabIfNotCollapsed, + sTestCanAnnotateDirectParentOfRoot, + sTestCanAnnotateBeforeTwoNonBreakingSpaces, + sTestCanAnnotateWithinTwoNonBreakingSpaces, + sTestCanAnnotateAfterTwoNonBreakingSpaces, + sTestInOneParagraph, + sTestInTwoParagraphs, + sTestInThreeParagraphs + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + setup: (ed: Editor) => { + ed.on('init', () => { + ed.annotator.register('test-annotation', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-anything': data.anything + }, + classes: [ ] + }; + } + }); + }); + } + }, success, failure); +}); \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationChangedTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationChangedTest.ts new file mode 100644 index 000000000..1f287be46 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationChangedTest.ts @@ -0,0 +1,323 @@ +import { Assertions, Chain, GeneralSteps, Logger, Pipeline, Step, Waiter } from '@ephox/agar'; +import { UnitTest } from '@ephox/bedrock'; +import { Cell } from '@ephox/katamari'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import { Editor } from 'tinymce/core/api/Editor'; +import ModernTheme from 'tinymce/themes/modern/Theme'; + +import { assertMarker, sAnnotate, sAssertHtmlContent } from '../../module/test/AnnotationAsserts'; + +UnitTest.asynctest('browser.tinymce.core.annotate.AnnotationChangedTest', (success, failure) => { + + ModernTheme(); + + const changes: CellThis |is the first paragraph
This is the second.
This is| the third.
Spanning |multiple
par||ag||raphs| now
' + tinyApis.sSetContent([ + 'This is the first paragraph
', + 'This is the second.
', + 'This is the third.
', + 'Spanning multiple
', + 'paragraphs now
' + ].join('')), + tinyApis.sSetSelection([ 0, 0 ], 'This '.length, [ 0, 0 ], 'This is'.length), + sAnnotate(editor, 'alpha', 'id-one', { anything: 'comment-1' }), + + tinyApis.sSetSelection([ 1, 0 ], 'T'.length, [ 1, 0 ], 'This is'.length), + sAnnotate(editor, 'alpha', 'id-two', { anything: 'comment-two' }), + + tinyApis.sSetSelection([ 2, 0 ], 'This is the th'.length, [ 2, 0 ], 'This is the thir'.length), + sAnnotate(editor, 'beta', 'id-three', { something: 'comment-three' }), + + tinyApis.sSetSelection([ 3, 0 ], 'Spanning '.length, [ 4, 0 ], 'paragraphs'.length), + sAnnotate(editor, 'gamma', 'id-four', { something: 'comment-four' }), + + tinyApis.sSetSelection([ 4, 0, 0 ], 'par'.length, [ 4, 0, 0 ], 'parag'.length ), + sAnnotate(editor, 'delta', 'id-five', { something: 'comment-five' }), + + Step.wait(1000), + sClearChanges, + + sAssertHtmlContent(tinyApis, [ + `This is the first paragraph
`, + + `This is the second.
`, + + `This is the third.
`, + + `Spanning multiple
`, + + `par` + + `ag` + + `raphs now
` + ]), + + // Outside: p(0) > text(0) > "Th".length + // Inside: p(0) > span(1) > text(0) > 'i'.length + // Inside: p(1) > span(1) > text(0), 'hi'.length + // Outside: p(1) > text(2) > ' the '.length + + Waiter.sTryUntil( + 'Waiting for no changes', + sAssertChanges('Should be no changes', [ ]), + 10, + 1000 + ), + + sTestAnnotationEvents( + 'No annotation at cursor', + [ 0, 0 ], 'Th'.length, + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null } + ] + ), + + sTestAnnotationEvents( + 'At annotation alpha, id = id-one', + [ 0, 1, 0 ], 'i'.length, + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' } + ] + ), + + sTestAnnotationEvents( + 'At annotation alpha, id = id-two', + [ 1, 1, 0 ], 'hi'.length, + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' }, + { state: true, name: 'alpha', uid: 'id-two' } + ] + ), + + tinyApis.sSetSelection([ 1, 1, 0 ], 'his'.length, [ 1, 1, 0 ], 'his'.length), + // Give it time to throttle a node change. + Step.wait(400), + Waiter.sTryUntil( + 'Moving selection within the same marker (alpha id-two) ... shoud not fire change', + sAssertChanges('checking changes', + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' }, + { state: true, name: 'alpha', uid: 'id-two' } + ] + ), + 10, + 1000 + ), + + sTestAnnotationEvents( + 'Outside annotations again', + [ 1, 2 ], ' the '.length, + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' }, + { state: true, name: 'alpha', uid: 'id-two' }, + { state: false, name: 'alpha', uid: null } + ] + ), + + sTestAnnotationEvents( + 'Inside annotation beta, id = id-three', + [ 2, 1, 0 ], 'i'.length, + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' }, + { state: true, name: 'alpha', uid: 'id-two' }, + { state: false, name: 'alpha', uid: null }, + { state: true, name: 'beta', uid: 'id-three' } + ] + ), + + tinyApis.sSetSelection([ 2, 0 ], 'T'.length, [ 2, 0 ], 'T'.length), + Waiter.sTryUntil( + 'Moving selection outside all annotations. Should fire null', + sAssertChanges('checking changes', + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' }, + { state: true, name: 'alpha', uid: 'id-two' }, + { state: false, name: 'alpha', uid: null }, + { state: true, name: 'beta', uid: 'id-three' }, + { state: false, name: 'beta', uid: null } + ] + ), + 10, + 1000 + ), + + tinyApis.sSetSelection([ 2, 2 ], 'd'.length, [ 2, 2 ], 'd'.length), + // Give it time to throttle a node change. + Step.wait(400), + Waiter.sTryUntil( + 'Moving selection outside all annotations (again). Should NOT fire null because it already has', + sAssertChanges('checking changes', + [ + { state: false, name: 'delta', uid: null }, + { state: false, name: 'gamma', uid: null }, + { state: true, name: 'alpha', uid: 'id-one' }, + { state: true, name: 'alpha', uid: 'id-two' }, + { state: false, name: 'alpha', uid: null }, + { state: true, name: 'beta', uid: 'id-three' }, + { state: false, name: 'beta', uid: null } + ] + ), + 10, + 1000 + ), + sClearChanges, + + tinyApis.sSetSelection([ 4, 0, 1, 0 ], 'a'.length, [ 4, 0, 1, 0 ], 'a'.length), + // Give it time to throttle a node change. + Step.wait(400), + Waiter.sTryUntil( + 'Moving selection inside delta (which is inside gamma)', + sAssertChanges('checking changes', + [ + { state: true, name: 'delta', uid: 'id-five' }, + { state: true, name: 'gamma', uid: 'id-four' } + ] + ), + 10, + 1000 + ), + + tinyApis.sSetSelection([ 4, 0, 0 ], 'p'.length, [ 4, 0, 0 ], 'p'.length), + // Give it time to throttle a node change. + Step.wait(400), + Waiter.sTryUntil( + 'Moving selection inside just gamma (but not delta)', + sAssertChanges('checking changes', + [ + { state: true, name: 'delta', uid: 'id-five' }, + { state: true, name: 'gamma', uid: 'id-four' }, + { state: false, name: 'delta', uid: null } + ] + ), + 10, + 1000 + ), + ]); + + Pipeline.async({}, [ + tinyApis.sFocus, + sTestChanges + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + setup: (ed: Editor) => { + ed.on('init', () => { + ed.annotator.register('alpha', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-anything': data.anything + }, + classes: [ ] + }; + } + }); + + ed.annotator.register('beta', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-something': data.something + }, + classes: [ ] + }; + } + }); + + ed.annotator.register('gamma', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-something': data.something + }, + classes: [ ] + }; + } + }); + + ed.annotator.register('delta', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-something': data.something + }, + classes: [ 'delta-test' ] + }; + } + }); + + // NOTE: Have to use old function syntax here when accessing "arguments" + const listener = function (state, name, obj) { + // NOTE: These failures won't stop the tests, but they will stop it before it updates + // the changes in changes.set + if (state === false) { + Assertions.assertEq('Argument count must be "2" (state, name) if state is false', 2, arguments.length); + } else { + const { uid, nodes } = obj; + // In this test, gamma markers span multiple nodes + if (name === 'gamma') { Assertions.assertEq('Gamma annotations must have 2 nodes', 2, nodes.length); } + assertMarker(ed, { uid, name }, nodes); + } + + changes.set( + changes.get().concat([ + { uid: state ? obj.uid : null, name, state } + ]) + ); + }; + + ed.annotator.annotationChanged('alpha', listener); + ed.annotator.annotationChanged('beta', listener); + ed.annotator.annotationChanged('gamma', listener); + ed.annotator.annotationChanged('delta', listener); + }); + } + }, success, failure); +}); \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationPersistenceTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationPersistenceTest.ts new file mode 100644 index 000000000..e3a980a5a --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationPersistenceTest.ts @@ -0,0 +1,131 @@ +import { Assertions, Chain, GeneralSteps, Logger, Pipeline, Step } from '@ephox/agar'; +import { UnitTest } from '@ephox/bedrock'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import { AnnotatorSettings } from 'tinymce/core/api/Annotator'; +import { Editor } from 'tinymce/core/api/Editor'; +import ModernTheme from 'tinymce/themes/modern/Theme'; + +import { sAnnotate } from '../../module/test/AnnotationAsserts'; + +UnitTest.asynctest('browser.tinymce.core.annotate.AnnotationPersistenceTest', (success, failure) => { + ModernTheme(); + + const sUndoLevel = (editor: Editor) => Step.sync(() => { + editor.undoManager.add(); + }); + + const sRunTinyWithSettings = (annotation: AnnotatorSettings, getSteps: (tinyApis: any, editor: Editor) => any[]) => Step.async((next, die) => { + const settings = { + skin_url: '/project/js/tinymce/skins/lightgray', + setup: (ed: Editor) => { + ed.on('init', () => { + ed.annotator.register('test-annotation', annotation); + }); + } + }; + TinyLoader.setup((editor: Editor, onSuccess, onFailure) => { + const tinyApis = TinyApis(editor); + Pipeline.async({}, getSteps(tinyApis, editor), onSuccess, onFailure); + }, settings, next, die); + }); + + const settingsWithPersistence = { + persistent: true, + decorate: (uid, data) => { + return { + attributes: { + 'data-test-anything': data.anything + }, + classes: [ ] + }; + } + }; + + const settingsWithDefaultPersistence = { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-anything': data.anything + }, + classes: [ ] + }; + } + }; + + const settingsWithoutPersistence = { + persistent: false, + decorate: (uid, data) => { + return { + attributes: { + 'data-test-anything': data.anything + }, + classes: [ ] + }; + } + }; + + const sSetupSingleAnnotation = (tinyApis, editor) => GeneralSteps.sequence([ + // 'This is the only p|ar|agraph
' + tinyApis.sSetContent('This is the only paragraph here
'), + sUndoLevel(editor), + tinyApis.sSetSelection([ 0, 0 ], 'This is the only p'.length, [ 0, 0 ], 'This is the only par'.length), + sAnnotate(editor, 'test-annotation', 'test-uid', { anything: 'one-paragraph' }), + tinyApis.sAssertContentPresence({ + '.mce-annotation': 1, + 'p:contains("This is the only paragraph here")': 1 + }), + Step.sync(() => { + editor.execCommand('undo'); + }), + tinyApis.sAssertContentPresence({ + '.mce-annotation': 0, + 'p:contains("This is the only paragraph here")': 1 + }), + Step.sync(() => { + editor.execCommand('redo'); + }), + tinyApis.sAssertContentPresence({ + '.mce-annotation': 1, + 'p:contains("This is the only paragraph here")': 1 + }) + ]); + + const sContentContains = (tinyApis: any, ed: Editor, pattern: string, isContained: boolean) => { + return Chain.asStep({ }, [ + Chain.mapper(() => ed.getContent()), + Chain.op((content) => { + Assertions.assertEq( + 'editor.getContent() should contain: ' + pattern + ' = ' + isContained, + true, + content.indexOf(pattern) > -1 === isContained + ); + }) + ]); + }; + + Pipeline.async({ }, [ + Logger.t( + 'Testing configuration with persistence', + sRunTinyWithSettings(settingsWithPersistence, (tinyApis: any, ed: Editor) => [ + sSetupSingleAnnotation(tinyApis, ed), + sContentContains(tinyApis, ed, 'mce-annotation', true) + ]) + ), + + Logger.t( + 'Testing configuration with *no* persistence', + sRunTinyWithSettings(settingsWithoutPersistence, (tinyApis: any, ed: Editor) => [ + sSetupSingleAnnotation(tinyApis, ed), + sContentContains(tinyApis, ed, 'mce-annotation', false) + ]) + ), + + Logger.t( + 'Testing configuration with default persistence', + sRunTinyWithSettings(settingsWithDefaultPersistence, (tinyApis: any, ed: Editor) => [ + sSetupSingleAnnotation(tinyApis, ed), + sContentContains(tinyApis, ed, 'mce-annotation', true) + ]) + ), + ], () => success(), failure); +}); \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationRemovedTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationRemovedTest.ts new file mode 100644 index 000000000..d560c224c --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/annotate/AnnotationRemovedTest.ts @@ -0,0 +1,184 @@ +import { GeneralSteps, Pipeline, Step, Waiter, Logger } from '@ephox/agar'; +import { UnitTest } from '@ephox/bedrock'; +import { TinyApis, TinyLoader } from '@ephox/mcagar'; +import { Editor } from 'tinymce/core/api/Editor'; +import ModernTheme from 'tinymce/themes/modern/Theme'; + +import { sAnnotate, sAssertHtmlContent, sAssertGetAll } from '../../module/test/AnnotationAsserts'; + +UnitTest.asynctest('browser.tinymce.core.annotate.AnnotationRemovedTest', (success, failure) => { + ModernTheme(); + + TinyLoader.setup(function (editor: Editor, onSuccess, onFailure) { + const tinyApis = TinyApis(editor); + + const sSetupData = GeneralSteps.sequence([ + // 'This |is the first paragraph
This is the second.
This is| the third.
' + tinyApis.sSetContent('This was the first paragraph
This is the second.
This is the third.
'), + tinyApis.sSetSelection([ 0, 0 ], 'This '.length, [ 0, 0 ], 'This was'.length), + sAnnotate(editor, 'alpha', 'id-one', { anything: 'comment-1' }), + + tinyApis.sSetSelection([ 1, 0 ], 'T'.length, [ 1, 0 ], 'This is'.length), + sAnnotate(editor, 'alpha', 'id-two', { anything: 'comment-two' }), + + tinyApis.sSetSelection([ 2, 0 ], 'This is the th'.length, [ 2, 0 ], 'This is the thir'.length), + sAnnotate(editor, 'beta', 'id-three', { something: 'comment-three' }), + + sAssertHtmlContent(tinyApis, [ + `This was the first paragraph
`, + + `This is the second.
`, + + `This is the third.
` + ]) + ]); + + const outside1 = { path: [ 0, 0 ], offset: 'Th'.length }; + const inside1 = { path: [ 0, 1, 0 ], offset: 'i'.length }; + const inside3 = { path: [ 2, 1, 0 ], offset: 'i'.length }; + + // Outside: p(0) > text(0) > "Th".length + // Inside: p(0) > span(1) > text(0) > 'i'.length + // Inside: p(1) > span(1) > text(0), 'hi'.length + // Outside: p(1) > text(2) > ' the '.length + const sTestGetAndRemove = GeneralSteps.sequence([ + tinyApis.sSetSelection(outside1.path, outside1.offset, outside1.path, outside1.offset), + Waiter.sTryUntil( + 'Nothing active (outside1)', + tinyApis.sAssertContentPresence({ + '.mce-annotation': 3 + }), + 100, + 1000 + ), + + Logger.t( + 'There should be two alpha annotations', + sAssertGetAll(editor, { + 'id-one': 1, + 'id-two': 1 + }, 'alpha') + ), + + Logger.t( + 'There should be one beta annotation', + sAssertGetAll(editor, { + 'id-three': 1 + }, 'beta') + ), + + Step.sync(() => { + editor.annotator.remove('alpha'); + }), + + // Need to wait because nothing should have changed. If we don't wait, we'll get + // a false positive when the throttling makes the change delayed. + Step.wait(1000), + + Waiter.sTryUntil( + 'removed alpha, but was not inside alpha', + tinyApis.sAssertContentPresence({ + '.mce-annotation': 3 + }), + 100, + 1000 + ), + Logger.t( + 'There should be still be two alpha annotations (because remove only works if you are inside)', + sAssertGetAll(editor, { + 'id-one': 1, + 'id-two': 1 + }, 'alpha') + ), + Logger.t( + 'There should still be one beta annotation', + sAssertGetAll(editor, { + 'id-three': 1 + }, 'beta') + ), + + tinyApis.sSetSelection(inside3.path, inside3.offset, inside3.path, inside3.offset), + Step.sync(() => { + editor.annotator.remove('beta'); + }), + Waiter.sTryUntil( + 'removed beta', + tinyApis.sAssertContentPresence({ + '.mce-annotation': 2 + }), + 100, + 1000 + ), + + Logger.t( + 'There should be still be two alpha annotations (because cursor was inside beta)', + sAssertGetAll(editor, { + 'id-one': 1, + 'id-two': 1 + }, 'alpha') + ), + Logger.t( + 'There should be no beta annotations', + sAssertGetAll(editor, { }, 'beta') + ), + + tinyApis.sSetSelection(inside1.path, inside1.offset, inside1.path, inside1.offset), + Step.sync(() => { + editor.annotator.remove('alpha'); + }), + + Waiter.sTryUntil( + 'removed alpha, and was inside alpha', + tinyApis.sAssertContentPresence({ + '.mce-annotation': 1 + }), + 100, + 1000 + ), + + Logger.t( + 'There should now be just one alpha annotation (second one was removed)', + sAssertGetAll(editor, { + 'id-two': 1 + }, 'alpha') + ), + Logger.t( + 'There should be no beta annotations', + sAssertGetAll(editor, { }, 'beta') + ), + ]); + + Pipeline.async({}, [ + tinyApis.sFocus, + sSetupData, + sTestGetAndRemove + ], onSuccess, onFailure); + }, { + skin_url: '/project/js/tinymce/skins/lightgray', + setup: (ed: Editor) => { + ed.on('init', () => { + ed.annotator.register('alpha', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-anything': data.anything + }, + classes: [ ] + }; + } + }); + + ed.annotator.register('beta', { + decorate: (uid, data) => { + return { + attributes: { + 'data-test-something': data.something + }, + classes: [ ] + }; + } + }); + }); + } + }, success, failure); +}); \ No newline at end of file diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/api/SettingsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/api/SettingsTest.ts new file mode 100644 index 000000000..d4fb63a28 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/api/SettingsTest.ts @@ -0,0 +1,22 @@ +import { Assertions } from '@ephox/agar'; +import { Editor } from 'tinymce/core/api/Editor'; +import EditorManager from 'tinymce/core/api/EditorManager'; +import Settings from 'tinymce/core/api/Settings'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.test('browser.tinymce.core.api.SettingsTest', function () { + Assertions.assertEq('Should be default id', 'tinymce', Settings.getBodyId(new Editor('id', {}, EditorManager))); + Assertions.assertEq('Should be specified id', 'x', Settings.getBodyId(new Editor('id', { body_id: 'x' }, EditorManager))); + Assertions.assertEq('Should be specified id for ida', 'a', Settings.getBodyId(new Editor('ida', { body_id: 'ida=a,idb=b' }, EditorManager))); + Assertions.assertEq('Should be specified id for idb', 'b', Settings.getBodyId(new Editor('idb', { body_id: 'ida=a,idb=b' }, EditorManager))); + Assertions.assertEq('Should be default id for idc', 'tinymce', Settings.getBodyId(new Editor('idc', { body_id: 'ida=a,idb=b' }, EditorManager))); + + Assertions.assertEq('Should be default class', '', Settings.getBodyClass(new Editor('id', {}, EditorManager))); + Assertions.assertEq('Should be specified class', 'x', Settings.getBodyClass(new Editor('id', { body_class: 'x' }, EditorManager))); + Assertions.assertEq('Should be specified class for ida', 'a', Settings.getBodyClass(new Editor('ida', { body_class: 'ida=a,idb=b' }, EditorManager))); + Assertions.assertEq('Should be specified class for idb', 'b', Settings.getBodyClass(new Editor('idb', { body_class: 'ida=a,idb=b' }, EditorManager))); + Assertions.assertEq('Should be default class for idc', '', Settings.getBodyClass(new Editor('idc', { body_class: 'ida=a,idb=b' }, EditorManager))); + + Assertions.assertEq('Should default content_css_cors to false', false, Settings.shouldUseContentCssCors(new Editor('id', {}, EditorManager))); + Assertions.assertEq('Should return true if content_css_cors is set', true, Settings.shouldUseContentCssCors(new Editor('id', { content_css_cors: true }, EditorManager))); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/api/dom/RangeUtilsTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/api/dom/RangeUtilsTest.ts new file mode 100644 index 000000000..74b3794c6 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/api/dom/RangeUtilsTest.ts @@ -0,0 +1,51 @@ +import { Assertions, GeneralSteps, Logger, Pipeline, Step } from '@ephox/agar'; +import RangeUtils from 'tinymce/core/api/dom/RangeUtils'; +import DOMUtils from 'tinymce/core/api/dom/DOMUtils'; +import ViewBlock from '../../../module/test/ViewBlock'; +import { UnitTest } from '@ephox/bedrock'; + +UnitTest.asynctest('browser.tinymce.core.api.dom.RangeUtilsTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const DOM = DOMUtils.DOM; + const viewBlock = ViewBlock(); + + const createRange = function (sc, so, ec, eo) { + const rng = DOM.createRng(); + rng.setStart(sc, so); + rng.setEnd(ec, eo); + return rng; + }; + + const assertRange = function (expected, actual) { + Assertions.assertEq('startContainers should be equal', true, expected.startContainer === actual.startContainer); + Assertions.assertEq('startOffset should be equal', true, expected.startOffset === actual.startOffset); + Assertions.assertEq('endContainer should be equal', true, expected.endContainer === actual.endContainer); + Assertions.assertEq('endOffset should be equal', true, expected.endOffset === actual.endOffset); + }; + + const sTestDontNormalizeAtAnchors = Logger.t('Don\'t normalize at anchors', Step.sync(function () { + viewBlock.update('abc'); + + const rng1 = createRange(viewBlock.get().firstChild, 1, viewBlock.get().firstChild, 1); + const rng1Clone = rng1.cloneRange(); + Assertions.assertEq('label', false, RangeUtils(DOM).normalize(rng1)); + assertRange(rng1Clone, rng1); + + const rng2 = createRange(viewBlock.get().lastChild, 0, viewBlock.get().lastChild, 0); + const rng2Clone = rng2.cloneRange(); + Assertions.assertEq('label', false, RangeUtils(DOM).normalize(rng2)); + assertRange(rng2Clone, rng2); + })); + + const sTestNormalize = GeneralSteps.sequence([ + sTestDontNormalizeAtAnchors + ]); + + Pipeline.async({}, [ + sTestNormalize + ], function () { + viewBlock.detach(); + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/bookmark/BookmarksTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/bookmark/BookmarksTest.ts new file mode 100644 index 000000000..9f8a12644 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/bookmark/BookmarksTest.ts @@ -0,0 +1,182 @@ +import { Assertions, Logger, Pipeline, NamedChain, Chain, RawAssertions } from '@ephox/agar'; +import { Editor, ApiChains } from '@ephox/mcagar'; +import Theme from 'tinymce/themes/modern/Theme'; +import { UnitTest } from '@ephox/bedrock'; +import GetBookmark from 'tinymce/core/bookmark/GetBookmark'; +import { Hierarchy, Element, Remove, Html, SelectorFilter, Replication } from '@ephox/sugar'; +import { Result, Arr } from '@ephox/katamari'; +import { isRangeBookmark, isPathBookmark, isIdBookmark, isIndexBookmark, isStringPathBookmark } from 'tinymce/core/bookmark/BookmarkTypes'; +import ResolveBookmark from 'tinymce/core/bookmark/ResolveBookmark'; + +UnitTest.asynctest('browser.tinymce.core.bookmark.BookmarksTest', (success, failure) => { + Theme(); + + const cGetBookmark = (type: number, normalized: boolean) => { + return NamedChain.direct('editor', Chain.mapper((editor) => GetBookmark.getBookmark(editor.selection, type, normalized)), 'bookmark'); + }; + + const cGetFilledPersistentBookmark = (type: number, normalized: boolean) => { + return NamedChain.direct('editor', Chain.mapper((editor) => GetBookmark.getPersistentBookmark(editor.selection, true)), 'bookmark'); + }; + + const assertRawRange = function (element, rng, startPath, startOffset, endPath, endOffset) { + const startContainer = Hierarchy.follow(element, startPath).getOrDie(); + const endContainer = Hierarchy.follow(element, endPath).getOrDie(); + + Assertions.assertDomEq('Should be expected start container', startContainer, Element.fromDom(rng.startContainer)); + Assertions.assertEq('Should be expected start offset', startOffset, rng.startOffset); + Assertions.assertDomEq('Should be expected end container', endContainer, Element.fromDom(rng.endContainer)); + Assertions.assertEq('Should be expected end offset', endOffset, rng.endOffset); + }; + + const cBundleOp = (f) => { + return NamedChain.bundle((input) => { + f(input); + return Result.value(input); + }); + }; + + const cCreateNamedEditor = NamedChain.write('editor', Editor.cFromSettings({ + skin_url: '/project/js/tinymce/skins/lightgray' + })); + + const cSetupEditor = (content, startPath, startOffset, endPath, endOffset) => { + return NamedChain.read('editor', Chain.fromChains([ + ApiChains.cSetContent(content), + ApiChains.cSetSelection(startPath, startOffset, endPath, endOffset) + ])); + }; + + const cRemoveEditor = NamedChain.read('editor', Editor.cRemove); + + const cSetCursor = (path, offset) => NamedChain.read('editor', ApiChains.cSetCursor(path, offset)); + + const cResolveBookmark = cBundleOp((input) => { + const rng = ResolveBookmark.resolve(input.editor.selection, input.bookmark).getOrDie('Should be resolved'); + input.editor.selection.setRng(rng); + }); + + const cAssertSelection = (spath, soffset, fpath, foffset) => NamedChain.read('editor', ApiChains.cAssertSelection(spath, soffset, fpath, foffset)); + + const sBookmarkTest = (namedChains) => { + return Chain.asStep({}, [ + NamedChain.asChain(Arr.flatten([ + [ cCreateNamedEditor ], + namedChains, + [ cRemoveEditor ] + ]) + )]); + }; + + const cAssertRangeBookmark = (spath, soffset, fpath, foffset) => cBundleOp((input) => { + RawAssertions.assertEq('Should be a range bookmark', true, isRangeBookmark(input.bookmark)); + assertRawRange(Element.fromDom(input.editor.getBody()), input.bookmark.rng, spath, soffset, fpath, foffset); + }); + + const cAssertPathBookmark = (expectedStart, expectedEnd) => cBundleOp((input) => { + RawAssertions.assertEq('Should be a path bookmark', true, isPathBookmark(input.bookmark)); + RawAssertions.assertEq('Should be expected start path', expectedStart, input.bookmark.start); + RawAssertions.assertEq('Should be expected end path', expectedEnd, input.bookmark.end); + }); + + const cAssertIndexBookmark = (expectedName, expectedIndex) => cBundleOp((input) => { + RawAssertions.assertEq('Should be an index bookmark', true, isIndexBookmark(input.bookmark)); + RawAssertions.assertEq('Should be expected name', expectedName, input.bookmark.name); + RawAssertions.assertEq('Should be expected index', expectedIndex, input.bookmark.index); + }); + + const cAssertStringPathBookmark = (expectedStart, expectedEnd) => cBundleOp((input) => { + RawAssertions.assertEq('Should be a string bookmark', true, isStringPathBookmark(input.bookmark)); + RawAssertions.assertEq('Should be expected start', expectedStart, input.bookmark.start); + RawAssertions.assertEq('Should be expected end', expectedEnd, input.bookmark.end); + }); + + const cAssertIdBookmark = cBundleOp((input) => { + RawAssertions.assertEq('Should be an id bookmark', true, isIdBookmark(input.bookmark)); + }); + + const cAssertApproxRawContent = (expectedHtml) => NamedChain.read('editor', Chain.op((editor) => { + const elm = Replication.deep(Element.fromDom(editor.getBody())); + Arr.each(SelectorFilter.descendants(elm, '*[data-mce-bogus="all"]'), Remove.remove); + const actualHtml = Html.get(elm); + Assertions.assertHtmlStructure('Should expected structure', `${expectedHtml}`, `${actualHtml}`); + })); + + Pipeline.async({}, [ + Logger.t('Range bookmark', sBookmarkTest([ + cSetupEditor('a
', [0, 0], 0, [0, 0], 1), + cGetBookmark(1, false), + cAssertRangeBookmark([0, 0], 0, [0, 0], 1), + cSetCursor([0, 0], 0), + cResolveBookmark, + cAssertSelection([0, 0], 0, [0, 0], 1) + ])), + Logger.t('Get path bookmark', sBookmarkTest([ + cSetupEditor('a
', [0, 0], 0, [0, 0], 1), + cGetBookmark(2, false), + cAssertPathBookmark([0, 0, 0], [1, 0, 0]), + cSetCursor([0, 0], 0), + cResolveBookmark, + cAssertSelection([0, 0], 0, [0, 0], 1) + ])), + Logger.t('Get id bookmark', sBookmarkTest([ + cSetupEditor('a
', [0, 0], 0, [0, 0], 1), + cGetBookmark(3, false), + cAssertStringPathBookmark('p[0]/text()[0],0', 'p[0]/text()[0],1'), + cSetCursor([0, 0], 0), + cResolveBookmark, + cAssertSelection([0, 0], 0, [0, 0], 1) + ])), + Logger.t('Get persistent bookmark on element indexes', sBookmarkTest([ + cSetupEditor('abc
', [0, 0], 1, [0, 0], 2), + cGetBookmark(0, false), + cAssertApproxRawContent('abc
'), + cAssertSelection([0, 2], 0, [0, 2], 1), + cAssertIdBookmark, + cSetCursor([0, 1], 0), + cResolveBookmark, + cAssertApproxRawContent('abc
'), + cAssertSelection([0, 0], 1, [0, 0], 2) + ])), + Logger.t('Get persistent bookmark marker spans on element indexes', sBookmarkTest([ + cSetupEditor('', [0], 0, [0], 2), + cGetBookmark(0, false), + cAssertApproxRawContent(''), + cAssertSelection([0], 1, [0], 3), + cAssertIdBookmark, + cSetCursor([0], 2), + cResolveBookmark, + cAssertApproxRawContent(''), + cAssertSelection([0], 0, [0], 2) + ])), + Logger.t('Get persistent bookmark filled with marker spans on text offsets', sBookmarkTest([ + cSetupEditor('
abc
', [0, 0], 1, [0, 0], 2), + cGetFilledPersistentBookmark(0, true), + cAssertApproxRawContent('a\ufeffb\ufeffc
'), + cAssertSelection([0, 1, 0], 1, [0, 3, 0], 1), + cAssertIdBookmark, + cSetCursor([0, 1], 0), + cResolveBookmark, + cAssertApproxRawContent('abc
'), + cAssertSelection([0, 0], 1, [0, 0], 2) + ])), + ], success, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/bookmark/CaretBookmarkTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/bookmark/CaretBookmarkTest.ts new file mode 100644 index 000000000..9d76eb00f --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/bookmark/CaretBookmarkTest.ts @@ -0,0 +1,174 @@ +import { LegacyUnit } from '@ephox/mcagar'; +import { Pipeline } from '@ephox/agar'; +import * as CaretBookmark from 'tinymce/core/bookmark/CaretBookmark'; +import CaretPosition from 'tinymce/core/caret/CaretPosition'; +import CaretAsserts from '../../module/test/CaretAsserts'; +import ViewBlock from '../../module/test/ViewBlock'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.CaretBookmarkTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + const viewBlock = ViewBlock(); + + const getRoot = function () { + return viewBlock.get(); + }; + + const setupHtml = function (html) { + viewBlock.update(html); + }; + + const createTextPos = function (textNode, offset) { + return CaretPosition(textNode, offset); + }; + + suite.test('create element index', function () { + setupHtml(''); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(getRoot().childNodes[0])), 'b[0],before'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(getRoot().childNodes[1])), 'i[0],before'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(getRoot().childNodes[2])), 'b[1],before'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.after(getRoot().childNodes[2])), 'b[1],after'); + }); + + suite.test('create text index', function () { + setupHtml('abccc'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[0], 0)), 'text()[0],0'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[2], 1)), 'text()[1],1'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[4], 3)), 'text()[2],3'); + }); + + suite.test('create text index on fragmented text nodes', function () { + setupHtml('a'); + getRoot().appendChild(document.createTextNode('b')); + getRoot().appendChild(document.createTextNode('c')); + getRoot().appendChild(document.createElement('b')); + getRoot().appendChild(document.createTextNode('d')); + getRoot().appendChild(document.createTextNode('e')); + + LegacyUnit.equal(getRoot().childNodes.length, 6); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[0], 0)), 'text()[0],0'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[1], 0)), 'text()[0],1'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[2], 0)), 'text()[0],2'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[4], 0)), 'text()[1],0'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), createTextPos(getRoot().childNodes[5], 0)), 'text()[1],1'); + }); + + suite.test('create br element index', function () { + setupHtml('a
'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(document.getElementById('a'))), 'p[0]/span[1]/b[0],before'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(document.getElementById('b'))), 'p[0]/span[1]/b[1],before'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(document.getElementById('c'))), 'p[0]/span[1]/b[2],before'); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.after(document.getElementById('c'))), 'p[0]/span[1]/b[2],after'); + }); + + suite.test('create deep text index', function () { + setupHtml('aabccc
'); + LegacyUnit.equal( + CaretBookmark.create(getRoot(), createTextPos(document.getElementById('x').childNodes[0], 0)), + 'p[0]/span[1]/text()[0],0' + ); + LegacyUnit.equal( + CaretBookmark.create(getRoot(), createTextPos(document.getElementById('x').childNodes[2], 1)), + 'p[0]/span[1]/text()[1],1' + ); + LegacyUnit.equal( + CaretBookmark.create(getRoot(), createTextPos(document.getElementById('x').childNodes[4], 3)), + 'p[0]/span[1]/text()[2],3' + ); + }); + + suite.test('create element index from bogus', function () { + setupHtml(''); + LegacyUnit.equal(CaretBookmark.create(getRoot(), CaretPosition.before(getRoot().lastChild.lastChild.childNodes[1])), 'b[3],before'); + }); + + suite.test('resolve element index', function () { + setupHtml(''); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'b[0],before'), CaretPosition.before(getRoot().childNodes[0])); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'b[1],before'), CaretPosition.before(getRoot().childNodes[2])); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'b[1],after'), CaretPosition.after(getRoot().childNodes[2])); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'i[0],before'), CaretPosition.before(getRoot().childNodes[1])); + }); + + suite.test('resolve odd element names', function () { + setupHtml('a
'); + CaretAsserts.assertCaretPosition( + CaretBookmark.resolve(getRoot(), 'p[0]/span[1]/b[0],before'), + CaretPosition.before(document.getElementById('a')) + ); + CaretAsserts.assertCaretPosition( + CaretBookmark.resolve(getRoot(), 'p[0]/span[1]/b[1],before'), + CaretPosition.before(document.getElementById('b')) + ); + CaretAsserts.assertCaretPosition( + CaretBookmark.resolve(getRoot(), 'p[0]/span[1]/b[2],before'), + CaretPosition.before(document.getElementById('c')) + ); + CaretAsserts.assertCaretPosition( + CaretBookmark.resolve(getRoot(), 'p[0]/span[1]/b[2],after'), + CaretPosition.after(document.getElementById('c')) + ); + }); + + suite.test('resolve text index', function () { + setupHtml('abccc'); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],0'), createTextPos(getRoot().childNodes[0], 0)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[1],1'), createTextPos(getRoot().childNodes[2], 1)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[2],3'), createTextPos(getRoot().childNodes[4], 3)); + }); + + suite.test('resolve text index on fragmented text nodes', function () { + setupHtml('a'); + getRoot().appendChild(document.createTextNode('b')); + getRoot().appendChild(document.createTextNode('c')); + getRoot().appendChild(document.createElement('b')); + getRoot().appendChild(document.createTextNode('d')); + getRoot().appendChild(document.createTextNode('e')); + + LegacyUnit.equal(getRoot().childNodes.length, 6); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],0'), createTextPos(getRoot().childNodes[0], 0)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],1'), createTextPos(getRoot().childNodes[0], 1)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],2'), createTextPos(getRoot().childNodes[1], 1)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],3'), createTextPos(getRoot().childNodes[2], 1)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],4'), createTextPos(getRoot().childNodes[2], 1)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[1],0'), createTextPos(getRoot().childNodes[4], 0)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[1],1'), createTextPos(getRoot().childNodes[4], 1)); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[1],2'), createTextPos(getRoot().childNodes[5], 1)); + }); + + suite.test('resolve text index with to high offset', function () { + setupHtml('abc'); + CaretAsserts.assertCaretPosition(CaretBookmark.resolve(getRoot(), 'text()[0],10'), createTextPos(getRoot().childNodes[0], 3)); + }); + + suite.test('resolve invalid paths', function () { + setupHtml(''); + LegacyUnit.equal(CaretBookmark.resolve(getRoot(), 'x[0]/y[1]/z[2]'), null); + LegacyUnit.equal(CaretBookmark.resolve(getRoot(), 'b[0]/i[2]'), null); + LegacyUnit.equal(CaretBookmark.resolve(getRoot(), 'x'), null); + LegacyUnit.equal(CaretBookmark.resolve(getRoot(), null), null); + }); + + viewBlock.attach(); + Pipeline.async({}, suite.toSteps({}), function () { + viewBlock.detach(); + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretCandidateTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretCandidateTest.ts new file mode 100644 index 000000000..6bf34ecc3 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretCandidateTest.ts @@ -0,0 +1,80 @@ +import { LegacyUnit } from '@ephox/mcagar'; +import { Pipeline } from '@ephox/agar'; +import Env from 'tinymce/core/api/Env'; +import * as CaretCandidate from 'tinymce/core/caret/CaretCandidate'; +import $ from 'tinymce/core/api/dom/DomQuery'; +import Zwsp from 'tinymce/core/text/Zwsp'; +import ViewBlock from '../../module/test/ViewBlock'; +import { UnitTest } from '@ephox/bedrock'; +import { document } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.CaretCandidateTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + const viewBlock = ViewBlock(); + + if (!Env.ceFalse) { + return; + } + + const getRoot = function () { + return viewBlock.get(); + }; + + const setupHtml = function (html) { + viewBlock.update(html); + }; + + suite.test('isCaretCandidate', function () { + $.each('img input textarea hr table iframe video audio object'.split(' '), function (index, name) { + LegacyUnit.equal(CaretCandidate.isCaretCandidate(document.createElement(name)), true); + }); + + LegacyUnit.equal(CaretCandidate.isCaretCandidate(document.createTextNode('text')), true); + LegacyUnit.equal(CaretCandidate.isCaretCandidate($('')[0]), true); + LegacyUnit.equal(CaretCandidate.isCaretCandidate($('')[0]), false); + LegacyUnit.equal(CaretCandidate.isCaretCandidate($('')[0]), true); + LegacyUnit.equal(CaretCandidate.isCaretCandidate($('| X |
| X |
a
'); + }); + + suite.test('prependInline', function () { + setupHtml('a'); + const caretContainerTextNode = CaretContainer.prependInline(getRoot().firstChild) as Text; + LegacyUnit.equal(caretContainerTextNode.data, Zwsp.ZWSP + 'a'); + }); + + suite.test('prependInline 2', function () { + setupHtml('a'); + LegacyUnit.equal(CaretContainer.prependInline(getRoot().firstChild), null); + LegacyUnit.equal(CaretContainer.prependInline(null), null); + }); + + suite.test('appendInline', function () { + setupHtml('a'); + const caretContainerTextNode = CaretContainer.appendInline(getRoot().firstChild) as Text; + LegacyUnit.equal(caretContainerTextNode.data, 'a' + Zwsp.ZWSP); + }); + + suite.test('isBeforeInline', function () { + setupHtml(Zwsp.ZWSP + 'a'); + LegacyUnit.equal(CaretContainer.isBeforeInline(CaretPosition(getRoot().firstChild, 0)), true); + LegacyUnit.equal(CaretContainer.isBeforeInline(CaretPosition(getRoot().firstChild, 1)), false); + }); + + suite.test('isAfterInline', function () { + setupHtml(Zwsp.ZWSP + 'a'); + LegacyUnit.equal(CaretContainer.isAfterInline(CaretPosition(getRoot().firstChild, 1)), true); + LegacyUnit.equal(CaretContainer.isAfterInline(CaretPosition(getRoot().firstChild, 0)), false); + }); + + viewBlock.attach(); + Pipeline.async({}, suite.toSteps({}), function () { + viewBlock.detach(); + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretFinderTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretFinderTest.ts new file mode 100644 index 000000000..708262ec8 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretFinderTest.ts @@ -0,0 +1,381 @@ +import { Assertions, Chain, GeneralSteps, Logger, Pipeline } from '@ephox/agar'; +import { Hierarchy, Element } from '@ephox/sugar'; +import CaretFinder from 'tinymce/core/caret/CaretFinder'; +import CaretPosition from 'tinymce/core/caret/CaretPosition'; +import ViewBlock from '../../module/test/ViewBlock'; +import { UnitTest } from '@ephox/bedrock'; +import { Option } from '@ephox/katamari'; + +UnitTest.asynctest('browser.tinymce.core.CaretFinderTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const viewBlock = ViewBlock(); + + const cSetHtml = function (html) { + return Chain.op(function () { + viewBlock.update(html); + }); + }; + + const cCreateFromPosition = function (path, offset) { + return Chain.mapper(function (viewBlock: any) { + const container = Hierarchy.follow(Element.fromDom(viewBlock.get()), path).getOrDie(); + return CaretPosition(container.dom(), offset); + }); + }; + + const cAssertCaretPosition = function (path, expectedOffset) { + return Chain.op(function (posOption: Optiona
'), + cCreateFromPosition([], 0), + cFromPosition(true), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should walk to last text node offset', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cCreateFromPosition([], 1), + cFromPosition(false), + cAssertCaretPosition([0, 0], 1) + ])), + Logger.t('Should walk to from text node offset 0 to 1', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cCreateFromPosition([0, 0], 0), + cFromPosition(true), + cAssertCaretPosition([0, 0], 1) + ])), + Logger.t('Should walk to from text node offset 1 to 0', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cCreateFromPosition([0, 0], 1), + cFromPosition(false), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([], 0), + cFromPosition(false), + cAssertNone + ])) + ])), + + Logger.t('navigate', GeneralSteps.sequence([ + Logger.t('navigate - forward', GeneralSteps.sequence([ + Logger.t('Should walk to second offset in text inside b', Chain.asStep(viewBlock, [ + cSetHtml('ab
'), + cCreateFromPosition([0, 0], 1), + cNavigate(true), + cAssertCaretPosition([0, 1, 0], 1) + ])), + Logger.t('Should walk from last text position in one b into the second text position in another b', Chain.asStep(viewBlock, [ + cSetHtml('ab
'), + cCreateFromPosition([0, 0, 0], 1), + cNavigate(true), + cAssertCaretPosition([0, 1, 0], 1) + ])), + Logger.t('Should walk to after input in b', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cCreateFromPosition([0, 0], 1), + cNavigate(true), + cAssertCaretPosition([0, 1], 1) + ])), + Logger.t('Should walk from after input to after input in b', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(true), + cAssertCaretPosition([0, 1], 1) + ])), + Logger.t('Should walk from after input inside b to after input in another b', Chain.asStep(viewBlock, [ + cSetHtml('
'), + cCreateFromPosition([0, 0], 1), + cNavigate(true), + cAssertCaretPosition([0, 1], 1) + ])), + Logger.t('Should walk from after input to second text offset in b', Chain.asStep(viewBlock, [ + cSetHtml('
a
'), + cCreateFromPosition([0], 1), + cNavigate(true), + cAssertCaretPosition([0, 1, 0], 1) + ])), + Logger.t('Should walk from over input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 0), + cNavigate(true), + cAssertCaretPosition([0], 1) + ])), + Logger.t('Should walk from before first input to after first input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 0), + cNavigate(true), + cAssertCaretPosition([0], 1) + ])), + Logger.t('Should walk from after first input to after second input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(true), + cAssertCaretPosition([0], 2) + ])), + Logger.t('Should walk from after first input to after second input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(true), + cAssertCaretPosition([0], 2) + ])), + Logger.t('should walk from after last text node offset to before CEF span', Chain.asStep(viewBlock, [ + cSetHtml('a
b
a
b
b
a
b
'), + cCreateFromPosition([0, 0], 1), + cNavigate(true), + cAssertCaretPosition([1, 0], 0) + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([], 0), + cNavigate(true), + cAssertNone + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cCreateFromPosition([0, 0], 1), + cNavigate(true), + cAssertNone + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(true), + cAssertNone + ])) + ])), + + Logger.t('navigate - backwards', GeneralSteps.sequence([ + Logger.t('Should walk to first offset in text inside b', Chain.asStep(viewBlock, [ + cSetHtml('ab
'), + cCreateFromPosition([0, 1], 0), + cNavigate(false), + cAssertCaretPosition([0, 0, 0], 0) + ])), + Logger.t('Should walk from last text position in one b into the second text position in another b', Chain.asStep(viewBlock, [ + cSetHtml('ab
'), + cCreateFromPosition([0, 1, 0], 0), + cNavigate(false), + cAssertCaretPosition([0, 0, 0], 0) + ])), + Logger.t('Should walk to before input in b', Chain.asStep(viewBlock, [ + cSetHtml('b
'), + cCreateFromPosition([0, 1], 0), + cNavigate(false), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should walk from before input to before input in b', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(false), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should walk from before input inside b to before input in another b', Chain.asStep(viewBlock, [ + cSetHtml('
'), + cCreateFromPosition([0, 1], 0), + cNavigate(false), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should walk from before input to first text offset in b', Chain.asStep(viewBlock, [ + cSetHtml('
a
'), + cCreateFromPosition([0], 1), + cNavigate(false), + cAssertCaretPosition([0, 0, 0], 0) + ])), + Logger.t('Should walk from over input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(false), + cAssertCaretPosition([0], 0) + ])), + Logger.t('Should walk from after last input to after first input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 2), + cNavigate(false), + cAssertCaretPosition([0], 1) + ])), + Logger.t('Should from after first input to before first input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 1), + cNavigate(false), + cAssertCaretPosition([0], 0) + ])), + Logger.t('Should from before last input to after first input', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 2), + cNavigate(false), + cAssertCaretPosition([0], 1) + ])), + Logger.t('Should walk from first text node offset over br to last text node offset', Chain.asStep(viewBlock, [ + cSetHtml('a
b
a
a
b
'), + cCreateFromPosition([1, 0], 0), + cNavigate(false), + cAssertCaretPosition([0, 0], 1) + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([], 0), + cNavigate(false), + cAssertNone + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cCreateFromPosition([0, 0], 0), + cNavigate(false), + cAssertNone + ])), + Logger.t('Should not walk anywhere since there is nothing to walk to', Chain.asStep(viewBlock, [ + cSetHtml(''), + cCreateFromPosition([0], 0), + cNavigate(false), + cAssertNone + ])), + Logger.t('Should jump over bogus elements', Chain.asStep(viewBlock, [ + cSetHtml([ + '1
', + '', + '2
' + ].join('')), + cCreateFromPosition([], 2), + cNavigate(false), + cAssertCaretPosition([0, 0], 1) + ])), + ])) + ])), + + Logger.t('positionIn', GeneralSteps.sequence([ + Logger.t('Should walk to first text node offset', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cPositionIn(true, [0]), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should walk to last text node offset', Chain.asStep(viewBlock, [ + cSetHtml('a
'), + cPositionIn(false, [0]), + cAssertCaretPosition([0, 0], 1) + ])), + Logger.t('Should walk to first element offset', Chain.asStep(viewBlock, [ + cSetHtml(''), + cPositionIn(true, [0]), + cAssertCaretPosition([0], 0) + ])), + Logger.t('Should walk to last element offset', Chain.asStep(viewBlock, [ + cSetHtml(''), + cPositionIn(false, [0]), + cAssertCaretPosition([0], 1) + ])), + Logger.t('Should walk to last element offset skip br', Chain.asStep(viewBlock, [ + cSetHtml(''), + cPositionIn(true, [0]), + cAssertCaretPosition([0, 0], 0) + ])), + Logger.t('Should walk to last inner element offset', Chain.asStep(viewBlock, [ + cSetHtml('
'), + cPositionIn(false, [0]), + cAssertCaretPosition([0, 0], 1) + ])), + Logger.t('Should not find any position in an empty element', Chain.asStep(viewBlock, [ + cSetHtml(''), + cPositionIn(true, [0]), + cAssertNone + ])), + Logger.t('Should not find any position in an empty element', Chain.asStep(viewBlock, [ + cSetHtml(''), + cPositionIn(false, [0]), + cAssertNone + ])), + Logger.t('Should not find any position in an empty element and not walk outside backwards', Chain.asStep(viewBlock, [ + cSetHtml('
a
b
'), + cPositionIn(false, [1]), + cAssertNone + ])), + Logger.t('Should not find any position in an empty element and not walk outside forwards', Chain.asStep(viewBlock, [ + cSetHtml('a
b
'), + cPositionIn(true, [1]), + cAssertNone + ])), + Logger.t('Should walk past comment node backwards', Chain.asStep(viewBlock, [ + cSetHtml('b
'), + cPositionIn(false, []), + cAssertCaretPosition([0, 1], 1) + ])), + Logger.t('Should walk past comment node forwards', Chain.asStep(viewBlock, [ + cSetHtml('b
'), + cPositionIn(true, []), + cAssertCaretPosition([0, 1], 0) + ])) + ])) + ], function () { + viewBlock.detach(); + success(); + }, failure); +}); diff --git a/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretPositionTest.ts b/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretPositionTest.ts new file mode 100644 index 000000000..c2d210886 --- /dev/null +++ b/tools-ng/tinymce/editor/src/core/test/ts/browser/caret/CaretPositionTest.ts @@ -0,0 +1,220 @@ +import { LegacyUnit } from '@ephox/mcagar'; +import { Pipeline } from '@ephox/agar'; +import Env from 'tinymce/core/api/Env'; +import CaretPosition from 'tinymce/core/caret/CaretPosition'; +import CaretAsserts from '../../module/test/CaretAsserts'; +import ViewBlock from '../../module/test/ViewBlock'; +import { UnitTest } from '@ephox/bedrock'; +import { Text } from '@ephox/dom-globals'; + +UnitTest.asynctest('browser.tinymce.core.CaretPositionTest', function () { + const success = arguments[arguments.length - 2]; + const failure = arguments[arguments.length - 1]; + const suite = LegacyUnit.createSuite(); + const createRange = CaretAsserts.createRange; + const viewBlock = ViewBlock(); + + if (!Env.ceFalse) { + return; + } + + const getRoot = function () { + return viewBlock.get(); + }; + + const setupHtml = function (html) { + viewBlock.update(html); + }; + + suite.test('Constructor', function () { + setupHtml('abc'); + LegacyUnit.equalDom(CaretPosition(getRoot(), 0).container(), getRoot()); + LegacyUnit.strictEqual(CaretPosition(getRoot(), 1).offset(), 1); + LegacyUnit.equalDom(CaretPosition(getRoot().firstChild, 0).container(), getRoot().firstChild); + LegacyUnit.strictEqual(CaretPosition(getRoot().firstChild, 1).offset(), 1); + }); + + suite.test('fromRangeStart', function () { + setupHtml('abc'); + CaretAsserts.assertCaretPosition(CaretPosition.fromRangeStart(createRange(getRoot(), 0)), CaretPosition(getRoot(), 0)); + CaretAsserts.assertCaretPosition(CaretPosition.fromRangeStart(createRange(getRoot(), 1)), CaretPosition(getRoot(), 1)); + CaretAsserts.assertCaretPosition( + CaretPosition.fromRangeStart(createRange(getRoot().firstChild, 1)), + CaretPosition(getRoot().firstChild, 1) + ); + }); + + suite.test('fromRangeEnd', function () { + setupHtml('abc'); + CaretAsserts.assertCaretPosition( + CaretPosition.fromRangeEnd(createRange(getRoot(), 0, getRoot(), 1)), + CaretPosition(getRoot(), 1) + ); + CaretAsserts.assertCaretPosition( + CaretPosition.fromRangeEnd(createRange(getRoot().firstChild, 0, getRoot().firstChild, 1)), + CaretPosition(getRoot().firstChild, 1) + ); + }); + + suite.test('after', function () { + setupHtml('abc123'); + CaretAsserts.assertCaretPosition(CaretPosition.after(getRoot().firstChild), CaretPosition(getRoot(), 1)); + CaretAsserts.assertCaretPosition(CaretPosition.after(getRoot().lastChild), CaretPosition(getRoot(), 2)); + }); + + suite.test('before', function () { + setupHtml('abc123'); + CaretAsserts.assertCaretPosition(CaretPosition.before(getRoot().firstChild), CaretPosition(getRoot(), 0)); + CaretAsserts.assertCaretPosition(CaretPosition.before(getRoot().lastChild), CaretPosition(getRoot(), 1)); + }); + + suite.test('isAtStart', function () { + setupHtml('abc123123'); + LegacyUnit.equal(CaretPosition(getRoot(), 0).isAtStart(), true); + LegacyUnit.equal(!CaretPosition(getRoot(), 1).isAtStart(), true); + LegacyUnit.equal(!CaretPosition(getRoot(), 3).isAtStart(), true); + LegacyUnit.equal(CaretPosition(getRoot().firstChild, 0).isAtStart(), true); + LegacyUnit.equal(!CaretPosition(getRoot().firstChild, 1).isAtStart(), true); + LegacyUnit.equal(!CaretPosition(getRoot().firstChild, 3).isAtStart(), true); + }); + + suite.test('isAtEnd', function () { + setupHtml('abc123123'); + LegacyUnit.equal(CaretPosition(getRoot(), 3).isAtEnd(), true); + LegacyUnit.equal(!CaretPosition(getRoot(), 2).isAtEnd(), true); + LegacyUnit.equal(!CaretPosition(getRoot(), 0).isAtEnd(), true); + LegacyUnit.equal(CaretPosition(getRoot().firstChild, 3).isAtEnd(), true); + LegacyUnit.equal(!CaretPosition(getRoot().firstChild, 0).isAtEnd(), true); + LegacyUnit.equal(!CaretPosition(getRoot().firstChild, 1).isAtEnd(), true); + }); + + suite.test('toRange', function () { + setupHtml('abc'); + CaretAsserts.assertRange(CaretPosition(getRoot(), 0).toRange(), createRange(getRoot(), 0)); + CaretAsserts.assertRange(CaretPosition(getRoot(), 1).toRange(), createRange(getRoot(), 1)); + CaretAsserts.assertRange(CaretPosition(getRoot().firstChild, 1).toRange(), createRange(getRoot().firstChild, 1)); + }); + + suite.test('isEqual', function () { + setupHtml('abc'); + LegacyUnit.equal(CaretPosition(getRoot(), 0).isEqual(CaretPosition(getRoot(), 0)), true); + LegacyUnit.equal(CaretPosition(getRoot(), 1).isEqual(CaretPosition(getRoot(), 0)), false); + LegacyUnit.equal(CaretPosition(getRoot(), 0).isEqual(CaretPosition(getRoot().firstChild, 0)), false); + }); + + suite.test('isVisible', function () { + setupHtml(' abc'); + LegacyUnit.equal(CaretPosition(getRoot().firstChild.firstChild, 0).isVisible(), false); + LegacyUnit.equal(CaretPosition(getRoot().firstChild.firstChild, 3).isVisible(), true); + }); + + suite.test('getClientRects', function () { + setupHtml( + 'abc' + + '123
' + + 'a
b
abc