Files
odoo_source/addons/web_editor/static/tests/test_utils.js
T
Antoine Guenet f3c4f77f68 [REF] web_editor: abstract cell creation in bootstrap grid inliner tests
This commit abstracts the cell creation in the bootstrap grid inliner
tests so as to make it less wordy, easier to read, and easier to write.

X-original-commit: beceaf05f333596816623417f827ea8cdabc7097
Part-of: odoo/odoo#113784
2023-02-27 18:12:23 +01:00

901 lines
32 KiB
JavaScript

odoo.define('web_editor.test_utils', function (require) {
"use strict";
var ajax = require('web.ajax');
var MockServer = require('web.MockServer');
var testUtils = require('web.test_utils');
var OdooEditorLib = require('@web_editor/js/editor/odoo-editor/src/OdooEditor');
var Widget = require('web.Widget');
var Wysiwyg = require('web_editor.wysiwyg');
var options = require('web_editor.snippets.options');
const COLOR_PICKER_TEMPLATE = `
<colorpicker>
<div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">
<button data-color="o-color-1"/>
<button data-color="o-color-2"/>
<button data-color="o-color-3"/>
<button data-color="o-color-4"/>
<button data-color="o-color-5"/>
</div>
<div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">
<button class="o_btn_transparent"/>
<button data-color="black-25"/>
<button data-color="black-50"/>
<button data-color="black-75"/>
<button data-color="white-25"/>
<button data-color="white-50"/>
<button data-color="white-75"/>
</div>
<div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"/>
</colorpicker>
`;
const SNIPPETS_TEMPLATE = `
<h2 id="snippets_menu">Add blocks</h2>
<div id="o_scroll">
<div id="snippet_structure" class="o_panel">
<div class="o_panel_header">First Panel</div>
<div class="o_panel_body">
<div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/web_editor/static/src/img/snippets_thumbs/s_hr.svg">
<div class="s_hr pt32 pb32">
<hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>
</div>
</div>
<div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
<section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
<div class="container">
<div class="row">
<div class="col-lg-10 offset-lg-1 pt32 pb32">
<h2>Title</h2>
<p class="lead o_default_snippet_text">Content</p>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
<div id="snippet_options" class="d-none">
<div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"/>
<div data-js="content"
data-selector=".s_hr, .test_option_all"
data-drop-in=".note-editable"
data-drop-near="p, h1, h2, h3, blockquote, .s_hr"/>
<div data-js="sizing_y" data-selector=".s_hr, .test_option_all"/>
<div data-selector=".test_option_all">
<we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>
</div>
<div data-js="BackgroundImage" data-selector=".test_option_all">
<we-button data-choose-image="true" data-no-preview="true">
<i class="fa fa-picture-o"/> Background Image
</we-button>
</div>
<div data-js="option_test" data-selector=".s_hr">
<we-select string="Alignment">
<we-button data-select-class="align-items-start">Top</we-button>
<we-button data-select-class="align-items-center">Middle</we-button>
<we-button data-select-class="align-items-end">Bottom</we-button>
<we-button data-select-class="align-items-stretch">Equal height</we-button>
</we-select>
</div>
</div>`;
MockServer.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
* @private
* @returns {Promise}
*/
async _performRpc(route, args) {
if (args.model === "ir.ui.view" && args.method === 'render_public_asset') {
if (args.args[0] === "web_editor.colorpicker") {
return COLOR_PICKER_TEMPLATE;
}
if (args.args[0] === "web_editor.snippets") {
return SNIPPETS_TEMPLATE;
}
}
return this._super(...arguments);
},
});
/**
* Options with animation and edition for test.
*/
options.registry.option_test = options.Class.extend({
cleanForSave: function () {
this.$target.addClass('cleanForSave');
},
onBuilt: function () {
this.$target.addClass('built');
},
onBlur: function () {
this.$target.removeClass('focus');
},
onClone: function () {
this.$target.addClass('clone');
this.$target.removeClass('focus');
},
onFocus: function () {
this.$target.addClass('focus');
},
onMove: function () {
this.$target.addClass('move');
},
onRemove: function () {
this.$target.closest('.note-editable').addClass('snippet_has_removed');
},
});
/**
* Constructor WysiwygTest why editable and unbreakable node used in test.
*/
var WysiwygTest = Wysiwyg.extend({
_parentToDestroyForTest: null,
/**
* Override 'destroy' of discuss so that it calls 'destroy' on the parent.
*
* @override
*/
destroy: function () {
unpatch();
this._super();
this.$target.remove();
this._parentToDestroyForTest.destroy();
},
});
function patch() {
testUtils.mock.patch(ajax, {
loadAsset: function (xmlId) {
if (xmlId === 'template.assets') {
return Promise.resolve({
cssLibs: [],
cssContents: ['body {background-color: red;}']
});
}
if (xmlId === 'template.assets_all_style') {
return Promise.resolve({
cssLibs: $('link[href]:not([type="image/x-icon"])').map(function () {
return $(this).attr('href');
}).get(),
cssContents: ['body {background-color: red;}']
});
}
throw 'Wrong template';
},
});
}
function unpatch() {
testUtils.mock.unpatch(ajax);
}
/**
* @param {object} data
* @returns {object}
*/
function wysiwygData(data) {
return _.defaults({}, data, {
'ir.ui.view': {
fields: {
display_name: {
string: "Displayed name",
type: "char",
},
},
records: [],
render_template(args) {
if (args[0] === 'web_editor.colorpicker') {
return COLOR_PICKER_TEMPLATE;
}
if (args[0] === 'web_editor.snippets') {
return SNIPPETS_TEMPLATE;
}
},
},
'ir.attachment': {
fields: {
display_name: {
string: "display_name",
type: 'char',
},
description: {
string: "description",
type: 'char',
},
mimetype: {
string: "mimetype",
type: 'char',
},
checksum: {
string: "checksum",
type: 'char',
},
url: {
string: "url",
type: 'char',
},
type: {
string: "type",
type: 'char',
},
res_id: {
string: "res_id",
type: 'integer',
},
res_model: {
string: "res_model",
type: 'char',
},
public: {
string: "public",
type: 'boolean',
},
access_token: {
string: "access_token",
type: 'char',
},
image_src: {
string: "image_src",
type: 'char',
},
image_width: {
string: "image_width",
type: 'integer',
},
image_height: {
string: "image_height",
type: 'integer',
},
original_id: {
string: "original_id",
type: 'many2one',
relation: 'ir.attachment',
},
},
records: [{
id: 1,
name: 'image',
description: '',
mimetype: 'image/png',
checksum: false,
url: '/web/image/123/transparent.png',
type: 'url',
res_id: 0,
res_model: false,
public: true,
access_token: false,
image_src: '/web/image/123/transparent.png',
image_width: 256,
image_height: 256,
}],
generate_access_token: function () {
return;
},
},
});
}
/**
* Create the wysiwyg instance for test (contains patch, usefull ir.ui.view, snippets).
*
* @param {object} params
*/
async function createWysiwyg(params) {
patch();
params.data = wysiwygData(params.data);
var parent = new Widget();
await testUtils.mock.addMockEnvironment(parent, params);
var wysiwygOptions = _.extend({}, params.wysiwygOptions, {
recordInfo: {
context: {},
res_model: 'module.test',
res_id: 1,
},
useOnlyTestUnbreakable: params.useOnlyTestUnbreakable,
});
var wysiwyg = new WysiwygTest(parent, wysiwygOptions);
wysiwyg._parentToDestroyForTest = parent;
var $textarea = $('<textarea/>');
if (wysiwygOptions.value) {
$textarea.val(wysiwygOptions.value);
}
var selector = params.debug ? 'body' : '#qunit-fixture';
$textarea.prependTo($(selector));
if (params.debug) {
$('body').addClass('debug');
}
return wysiwyg.attachTo($textarea).then(function () {
if (wysiwygOptions.snippets) {
var defSnippets = testUtils.makeTestPromise();
testUtils.mock.intercept(wysiwyg, "snippets_loaded", function () {
defSnippets.resolve(wysiwyg);
});
return defSnippets;
}
return wysiwyg;
});
}
/**
* Char codes.
*/
var keyboardMap = {
"8": "BACKSPACE",
"9": "TAB",
"13": "ENTER",
"16": "SHIFT",
"17": "CONTROL",
"18": "ALT",
"19": "PAUSE",
"20": "CAPS_LOCK",
"27": "ESCAPE",
"32": "SPACE",
"33": "PAGE_UP",
"34": "PAGE_DOWN",
"35": "END",
"36": "HOME",
"37": "LEFT",
"38": "UP",
"39": "RIGHT",
"40": "DOWN",
"45": "INSERT",
"46": "DELETE",
"91": "OS_KEY", // 'left command': Windows Key (Windows) or Command Key (Mac)
"93": "CONTEXT_MENU", // 'right command'
};
_.each(_.range(40, 127), function (keyCode) {
if (!keyboardMap[keyCode]) {
keyboardMap[keyCode] = String.fromCharCode(keyCode);
}
});
/**
* Perform a series of tests (`keyboardTests`) for using keyboard inputs.
*
* @see wysiwyg_keyboard_tests.js
* @see wysiwyg_tests.js
*
* @param {jQuery} $editable
* @param {object} assert
* @param {object[]} keyboardTests
* @param {string} keyboardTests.name
* @param {string} keyboardTests.content
* @param {object[]} keyboardTests.steps
* @param {string} keyboardTests.steps.start
* @param {string} [keyboardTests.steps.end] default: steps.start
* @param {string} keyboardTests.steps.key
* @param {object} keyboardTests.test
* @param {string} [keyboardTests.test.content]
* @param {string} [keyboardTests.test.start]
* @param {string} [keyboardTests.test.end] default: steps.start
* @param {function($editable, assert)} [keyboardTests.test.check]
* @param {Number} addTests
*/
var testKeyboard = function ($editable, assert, keyboardTests, addTests) {
var tests = _.compact(_.pluck(keyboardTests, 'test'));
var testNumber = _.compact(_.pluck(tests, 'start')).length +
_.compact(_.pluck(tests, 'content')).length +
_.compact(_.pluck(tests, 'check')).length +
(addTests | 0);
assert.expect(testNumber);
function keydown(target, keypress) {
var $target = $(target.tagName ? target : target.parentNode);
if (!keypress.keyCode) {
keypress.keyCode = +_.findKey(keyboardMap, function (key) {
return key === keypress.key;
});
} else {
keypress.key = keyboardMap[keypress.keyCode] || String.fromCharCode(keypress.keyCode);
}
var event = $.Event("keydown", keypress);
$target.trigger(event);
if (!event.isDefaultPrevented()) {
if (keypress.key.length === 1) {
textInput($target[0], keypress.key);
} else {
console.warn('Native "' + keypress.key + '" is not supported in test');
}
}
$target.trigger($.Event("keyup", keypress));
return $target;
}
function _select(selector) {
// eg: ".class:contents()[0]->1" selects the first contents of the 'class' class, with an offset of 1
var reDOMSelection = /^(.+?)(:contents(\(\)\[|\()([0-9]+)[\]|\)])?(->([0-9]+))?$/;
var sel = selector.match(reDOMSelection);
var $node = $editable.find(sel[1]);
var point = {
node: sel[3] ? $node.contents()[+sel[4]] : $node[0],
offset: sel[5] ? +sel[6] : 0,
};
if (!point.node || point.offset > (point.node.tagName ? point.node.childNodes : point.node.textContent).length) {
assert.notOk("Node not found: '" + selector + "' " + (point.node ? "(container: '" + (point.node.outerHTML || point.node.textContent) + "')" : ""));
}
return point;
}
function selectText(start, end) {
start = _select(start);
var target = start.node;
$(target.tagName ? target : target.parentNode).trigger("mousedown");
if (end) {
end = _select(end);
Wysiwyg.setRange(start.node, start.offset, end.node, end.offset);
} else {
Wysiwyg.setRange(start.node, start.offset);
}
target = end ? end.node : start.node;
$(target.tagName ? target : target.parentNode).trigger('mouseup');
}
function nextPoint(point) {
var node, offset;
if (OdooEditorLib.nodeSize(point.node) === point.offset) {
node = point.node.parentNode;
offset = OdooEditorLib.childNodeIndex(point.node) + 1;
} else if (point.node.hasChildNodes()) {
node = point.node.childNodes[point.offset];
offset = 0;
} else {
node = point.node;
offset = point.offset + 1;
}
return {
node: node,
offset: offset
};
}
function endOfAreaBetweenTwoNodes(point) {
// move the position because some browser make the caret on the end of the previous area after normalize
if (
!point.node.tagName &&
point.offset === point.node.textContent.length &&
!/\S|\u00A0/.test(point.node.textContent)
) {
point = nextPoint(nextPoint(point));
while (point.node.tagName && point.node.textContent.length) {
point = nextPoint(point);
}
}
return point;
}
var defPollTest = Promise.resolve();
function pollTest(test) {
var def = Promise.resolve();
$editable.data('wysiwyg').setValue(test.content);
function poll(step) {
var def = testUtils.makeTestPromise();
if (step.start) {
selectText(step.start, step.end);
if (!Wysiwyg.getRange()) {
throw 'Wrong range! \n' +
'Test: ' + test.name + '\n' +
'Selection: ' + step.start + '" to "' + step.end + '"\n' +
'DOM: ' + $editable.html();
}
}
setTimeout(function () {
if (step.keyCode || step.key) {
var target = Wysiwyg.getRange().ec;
if (window.location.search.indexOf('notrycatch') !== -1) {
keydown(target, {
key: step.key,
keyCode: step.keyCode,
ctrlKey: !!step.ctrlKey,
shiftKey: !!step.shiftKey,
altKey: !!step.altKey,
metaKey: !!step.metaKey,
});
} else {
try {
keydown(target, {
key: step.key,
keyCode: step.keyCode,
ctrlKey: !!step.ctrlKey,
shiftKey: !!step.shiftKey,
altKey: !!step.altKey,
metaKey: !!step.metaKey,
});
} catch (e) {
assert.notOk(e.name + '\n\n' + e.stack, test.name);
}
}
}
setTimeout(function () {
if (step.keyCode || step.key) {
var $target = $(target.tagName ? target : target.parentNode);
$target.trigger($.Event('keyup', {
key: step.key,
keyCode: step.keyCode,
ctrlKey: !!step.ctrlKey,
shiftKey: !!step.shiftKey,
altKey: !!step.altKey,
metaKey: !!step.metaKey,
}));
}
setTimeout(def.resolve.bind(def));
});
});
return def;
}
while (test.steps.length) {
def = def.then(poll.bind(null, test.steps.shift()));
}
return def.then(function () {
if (!test.test) {
return;
}
if (test.test.check) {
test.test.check($editable, assert);
}
// test content
if (test.test.content) {
var value = $editable.data('wysiwyg').getValue({
keepPopover: true,
});
var allInvisible = /\u200B/g;
value = value.replace(allInvisible, '&#8203;');
var result = test.test.content.replace(allInvisible, '&#8203;');
assert.strictEqual(value, result, test.name);
if (test.test.start && value !== result) {
assert.notOk("Wrong DOM (see previous assert)", test.name + " (carret position)");
return;
}
}
$editable[0].normalize();
// test carret position
if (test.test.start) {
var start = _select(test.test.start);
var range = Wysiwyg.getRange();
if ((range.sc !== range.ec || range.so !== range.eo) && !test.test.end) {
assert.ok(false, test.name + ": the carret is not colapsed and the 'end' selector in test is missing");
return;
}
var end = test.test.end ? _select(test.test.end) : start;
if (start.node && end.node) {
range = Wysiwyg.getRange();
var startPoint = endOfAreaBetweenTwoNodes({
node: range.sc,
offset: range.so,
});
var endPoint = endOfAreaBetweenTwoNodes({
node: range.ec,
offset: range.eo,
});
var sameDOM = (startPoint.node.outerHTML || startPoint.node.textContent) === (start.node.outerHTML || start.node.textContent);
var stringify = function (obj) {
if (!sameDOM) {
delete obj.sameDOMsameNode;
}
return JSON.stringify(obj, null, 2)
.replace(/"([^"\s-]+)":/g, "\$1:")
.replace(/([^\\])"/g, "\$1'")
.replace(/\\"/g, '"');
};
assert.deepEqual(stringify({
startNode: startPoint.node.outerHTML || startPoint.node.textContent,
startOffset: startPoint.offset,
endPoint: endPoint.node.outerHTML || endPoint.node.textContent,
endOffset: endPoint.offset,
sameDOMsameNode: sameDOM && startPoint.node === start.node,
}),
stringify({
startNode: start.node.outerHTML || start.node.textContent,
startOffset: start.offset,
endPoint: end.node.outerHTML || end.node.textContent,
endOffset: end.offset,
sameDOMsameNode: true,
}),
test.name + " (carret position)");
}
}
});
}
while (keyboardTests.length) {
defPollTest = defPollTest.then(pollTest.bind(null, keyboardTests.shift()));
}
return defPollTest;
};
/**
* Select a node in the dom with is offset.
*
* @param {String} startSelector
* @param {String} endSelector
* @param {jQuery} $editable
* @returns {Object} {sc, so, ec, eo}
*/
var select = (function () {
var __select = function (selector, $editable) {
var sel = selector.match(/^(.+?)(:contents\(\)\[([0-9]+)\]|:contents\(([0-9]+)\))?(->([0-9]+))?$/);
var $node = $editable.find(sel[1]);
return {
node: sel[2] ? $node.contents()[sel[3] ? +sel[3] : +sel[4]] : $node[0],
offset: sel[5] ? +sel[6] : 0,
};
};
return function (startSelector, endSelector, $editable) {
var start = __select(startSelector, $editable);
var end = endSelector ? __select(endSelector, $editable) : start;
return {
sc: start.node,
so: start.offset,
ec: end.node,
eo: end.offset,
};
};
})();
/**
* Trigger a keydown event.
*
* @param {String or Number} key (name or code)
* @param {jQuery} $editable
* @param {Object} [options]
* @param {Boolean} [options.firstDeselect] (default: false) true to deselect before pressing
*/
var keydown = function (key, $editable, options) {
var keyPress = {};
if (typeof key === 'string') {
keyPress.key = key;
keyPress.keyCode = +_.findKey(keyboardMap, function (k) {
return k === key;
});
} else {
keyPress.key = keyboardMap[key] || String.fromCharCode(key);
keyPress.keyCode = key;
}
var range = Wysiwyg.getRange();
if (!range) {
console.error("Editor have not any range");
return;
}
if (options && options.firstDeselect) {
range.sc = range.ec;
range.so = range.eo;
Wysiwyg.setRange(range.sc, range.so, range.ec, range.eo);
}
var target = range.ec;
var $target = $(target.tagName ? target : target.parentNode);
var event = $.Event("keydown", keyPress);
$target.trigger(event);
if (!event.isDefaultPrevented()) {
if (keyPress.key.length === 1) {
textInput($target[0], keyPress.key);
} else {
console.warn('Native "' + keyPress.key + '" is not supported in test');
}
}
};
var textInput = function (target, char) {
var ev = new CustomEvent('textInput', {
bubbles: true,
cancelBubble: false,
cancelable: true,
composed: true,
data: char,
defaultPrevented: false,
detail: 0,
eventPhase: 3,
isTrusted: true,
returnValue: true,
sourceCapabilities: null,
type: "textInput",
which: 0,
});
ev.data = char;
target.dispatchEvent(ev);
if (!ev.defaultPrevented) {
document.execCommand("insertText", 0, ev.data);
}
};
//--------------------------------------------------------------------------
// Convert Inline
//--------------------------------------------------------------------------
const tableAttributes = {
cellspacing: 0,
cellpadding: 0,
border: 0,
width: '100%',
align: 'center',
role: 'presentation',
};
const tableAttributesString = Object.keys(tableAttributes).map(key => `${key}="${tableAttributes[key]}"`).join(' ');
const tableStyles = {
'border-collapse': 'collapse',
'text-align': 'inherit',
'font-size': 'unset',
'line-height': 'unset',
};
const tableStylesString = Object.keys(tableStyles).map(key => `${key}: ${tableStyles[key]};`).join(' ');
/**
* Take a matrix representing a grid and return an HTML string of the Bootstrap
* grid. The matrix is an array of rows, with each row being an array of cells.
* Each cell can be represented either by a 0 < number < 13 (col-#) or a falsy
* value (col). Each cell has its coordinates `(row index, column index)` as
* text content.
* Eg: [ // <div class="container">
* [ // <div class="row">
* 1, // <div class="col-1">(0, 0)</div>
* 11, // <div class="col-11">(0, 1)</div>
* ], // </div>
* [ // <div class="row">
* false, // <div class="col">(1, 0)</div>
* ], // </div>
* ] // </div>
*
* @param {Array<Array<Number|null>>} matrix
* @returns {string}
*/
function getGridHtml(matrix) {
return (
`<div class="container">` +
matrix.map((row, iRow) => (
`<div class="row">` +
row.map((col, iCol) => (
`<div class="${col ? 'col-' + col : 'col'}">(${iRow}, ${iCol})</div>`
)).join('') +
`</div>`
)).join('') +
`</div>`
);
}
function getTdHtml(colspan, text, containerWidth) {
return (
`<td colspan="${colspan}"${
containerWidth ? ' ' + `style="max-width: ${Math.round(containerWidth*colspan/12*100)/100}px;"`
: ''}>` +
text +
`</td>`
);
}
/**
* Take a matrix representing a table and return an HTML string of the table.
* The matrix is an array of rows, with each row being an array of cells. Each
* cell is represented by a tuple of numbers [colspan, width (in percent)]. A
* cell can have a string as third value to represent its text content. The
* default text content of each cell is its coordinates `(row index, column
* index)`. If the cell has a number as third value, it will be used as the
* max-width of the cell (in pixels).
* Eg: [ // <table> (note: extra attrs and styles apply)
* [ // <tr>
* [1, 8], // <td colspan="1" width="8%">(0, 0)</td>
* [11, 92] // <td colspan="11" width="92%">(0, 1)</td>
* ], // </tr>
* [ // <tr>
* [2, 17, 'A'], // <td colspan="2" width="17%">A</td>
* [10, 83], // <td colspan="10" width="83%">(1, 1)</td>
* ], // </tr>
* ] // </table>
*
* @param {Array<Array<Array<[Number, Number, string?, number?]>>>} matrix
* @param {Number} [containerWidth]
* @returns {string}
*/
function getTableHtml(matrix, containerWidth) {
return (
`<table ${tableAttributesString} style="width: 100% !important; ${tableStylesString}">` +
matrix.map((row, iRow) => (
`<tr>` +
row.map((col, iCol) => (
getTdHtml(col[0], typeof col[2] === 'string' ? col[2] : `(${iRow}, ${iCol})`, containerWidth)
)).join('') +
`</tr>`
)).join('') +
`</table>`
);
}
/**
* Take a number of rows and a number of columns (or number of columns per
* individual row) and return an HTML string of the corresponding grid. Every
* column is a regular Bootstrap "col" (no col-#).
* Eg: [2, 3] <=> getGridHtml([[false, false, false], [false, false, false]])
* Eg: [2, [2, 1]] <=> getGridHtml([[false, false], [false]])
*
* @see getGridHtml
* @param {Number} nRows
* @param {Number|Number[]} nCols
* @returns {string}
*/
function getRegularGridHtml(nRows, nCols) {
const matrix = new Array(nRows).fill().map((_, iRow) => (
new Array(Array.isArray(nCols) ? nCols[iRow] : nCols).fill()
));
return getGridHtml(matrix);
};
/**
* Take a number of rows, a number of columns (or number of columns per
* individual row), a colspan (or colspan per individual row) and a width (or
* width per individual row, in percent), and return an HTML string of the
* corresponding table. Every cell in a row has the same colspan/width.
* Eg: [2, 2, 6, 50] <=> getTableHtml([[[6, 50], [6, 50]], [[6, 50], [6, 50]]])
* Eg: [2, [2, 1], [6, 12], [50, 100]] <=> getTableHtml([[[6, 50], [6, 50]], [[12, 100]]])
*
* @see getTableHtml
* @param {Number} nRows
* @param {Number|Number[]} nCols
* @param {Number|Number[]} colspan
* @param {Number|Number[]} width
* @param {Number} containerWidth
* @returns {string}
*/
function getRegularTableHtml(nRows, nCols, colspan, width, containerWidth) {
const matrix = new Array(nRows).fill().map((_, iRow) => (
new Array(Array.isArray(nCols) ? nCols[iRow] : nCols).fill().map(() => ([
Array.isArray(colspan) ? colspan[iRow] : colspan,
Array.isArray(width) ? width[iRow] : width,
])))
);
return getTableHtml(matrix, containerWidth);
}
/**
* Take an HTML string and returns that string stripped from any HTML comments.
* By default, also removes the mso-hide class which is only there for outlook
* to hide elements when we use mso conditional comments.
*
* @param {string} html
* @param {boolean} [removeMsoHide=true]
* @returns {string}
*/
function removeComments(html, removeMsoHide=true) {
const cleanHtml = html.replace(/<!--(.*?)-->/g, '');
if (removeMsoHide) {
return cleanHtml.replaceAll(' class="mso-hide"', '').replace(/\s*mso-hide/g, '').replace(/mso-hide\s*/g, '');
} else {
return cleanHtml;
}
}
return {
wysiwygData: wysiwygData,
createWysiwyg: createWysiwyg,
testKeyboard: testKeyboard,
select: select,
keydown: keydown,
patch: patch,
unpatch: unpatch,
getGridHtml: getGridHtml,
getTableHtml: getTableHtml,
getRegularGridHtml: getRegularGridHtml,
getRegularTableHtml: getRegularTableHtml,
getTdHtml: getTdHtml,
removeComments: removeComments,
};
});