123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900 |
- 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 { TABLE_ATTRIBUTES, TABLE_STYLES } = require('@web_editor/js/backend/convert_inline');
- 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">
- <button data-color="black"></button>
- <button data-color="900"></button>
- <button data-color="800"></button>
- <button data-color="700" class="d-none"></button>
- <button data-color="600"></button>
- <button data-color="500" class="d-none"></button>
- <button data-color="400"></button>
- <button data-color="300" class="d-none"></button>
- <button data-color="200"></button>
- <button data-color="100"></button>
- <button data-color="white"></button>
- </div>
- </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, '​');
- var result = test.test.content.replace(allInvisible, '​');
- 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 tableAttributesString = Object.keys(TABLE_ATTRIBUTES).map(key => `${key}="${TABLE_ATTRIBUTES[key]}"`).join(' ');
- const tableStylesString = Object.keys(TABLE_STYLES).map(key => `${key}: ${TABLE_STYLES[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,
- };
- });
|