'],
44 |
45 | // Newlines between paragraphs in html have no syntactic value,
46 | // but then have a tendency to accidentally become additional paragraphs down the line
47 | [new RegExp(/<\/p>\n+/gi), ''],
48 | [new RegExp(/\n+
51 | [new RegExp(/<\/?o:[a-z]*>/gi), ''],
52 |
53 | // Microsoft Word adds some special elements around list items
54 | [new RegExp(/(((?!/gi), '$1']
55 | ];
56 | }
57 | };
58 |
59 | export default pasteUtils;
60 |
--------------------------------------------------------------------------------
/src/scripts/utils/string.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | /**
4 | * string utilities
5 | * @access protected
6 | */
7 | export default {
8 | capitalize (string) {
9 | return string.charAt(0).toUpperCase() + string.slice(1);
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/scripts/utils/zeroWidthSpace.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | const zeroWidthSpaceEntity = '​';
4 |
5 | /**
6 | * zeroWidthSpace -
7 | * utililties for generating and asserting zeroWidthSpace entities used as bookend
8 | * hooks when dynamically setting selection range around content.
9 | * @access protected
10 | */
11 | const zeroWidthSpace = {
12 | generate () {
13 | let tmpEl = document.createElement('span');
14 | tmpEl.innerHTML = zeroWidthSpaceEntity;
15 | return tmpEl;
16 | },
17 |
18 | get () {
19 | const tmpEl = zeroWidthSpace.generate();
20 | return tmpEl.firstChild;
21 | },
22 |
23 | assert (node) {
24 | const tmpEl = zeroWidthSpace.generate();
25 | if (node.nodeType === Node.ELEMENT_NODE) {
26 | return node.innerHTML === tmpEl.innerHTML;
27 | } else if (node.nodeType === Node.TEXT_NODE) {
28 | return node.nodeValue === tmpEl.firstChild.nodeValue;
29 | }
30 | }
31 | };
32 |
33 | export default zeroWidthSpace;
34 |
--------------------------------------------------------------------------------
/src/styles/canvas.scss:
--------------------------------------------------------------------------------
1 | .typester-canvas {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | height: 0;
6 | width: 0;
7 | opacity: 0;
8 | //debug styles
9 | // right: 0;
10 | // left: auto;
11 | // z-index: 100;
12 | // width: 33vw;
13 | // height: 100vh;
14 | // background: #DDD;
15 | // opacity: 2;
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/contentEditable.scss:
--------------------------------------------------------------------------------
1 | .typester-content-editable {
2 | &[data-placeholder] {
3 | &:before {
4 | content: attr(data-placeholder);
5 | display: none;
6 | color: rgb(160, 160, 160);
7 | position: absolute;
8 | }
9 |
10 | &.show-placeholder {
11 | &:before {
12 | display: block;
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/flyout.scss:
--------------------------------------------------------------------------------
1 | .typester-flyout {
2 | transition: top 200ms, left 200ms;
3 | position: absolute;
4 | z-index: 1600; // Bump up to ensure this displays above modals
5 | top: 50%;
6 | left: 50%;
7 |
8 | .typester-flyout-content {
9 | background: rgb(32, 31, 32);
10 | height: 40px;
11 | width: auto;
12 | }
13 |
14 | .typester-flyout-arrow {
15 | position: absolute;
16 | left: 50%;
17 | height: 0;
18 | width: 0;
19 | border-left: 10px solid transparent;
20 | border-right: 10px solid transparent;
21 | }
22 |
23 | &.place-above {
24 | transform: translate3d(-50%, -100%, 0);
25 | padding-bottom: 12px;
26 |
27 | .typester-flyout-content {}
28 |
29 | .typester-flyout-arrow {
30 | top: 100%;
31 | border-top: 10px solid rgb(32, 31, 32);
32 | transform: translate3d(-50%, -13px, 0);
33 | }
34 | }
35 |
36 | &.place-below {
37 | transform: translate3d(-50%, 0, 0);
38 | padding-top: 12px;
39 |
40 | .typester-flyout-content {}
41 |
42 | .typester-flyout-arrow {
43 | bottom: 100%;
44 | border-bottom: 10px solid rgb(32, 31, 32);
45 | transform: translate3d(-50%, 13px, 0);
46 | }
47 | }
48 |
49 | a {
50 | color: #FFF;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/styles/inputForm.scss:
--------------------------------------------------------------------------------
1 | .typester-input-form {
2 | input[type=text] {
3 | background: none;
4 | border: none;
5 | padding: 5px 15px;
6 | height: 30px;
7 | color: #FFF;
8 | width: 250px;
9 | outline: none;
10 | vertical-align: top;
11 | }
12 |
13 | button {
14 | height: 40px;
15 | width: 40px;
16 | line-height: 40px;
17 | background: none;
18 | border: none;
19 | color: #FFF;
20 | cursor: pointer;
21 | outline: none;
22 | text-align: center;
23 | padding: 0;
24 | margin: 0;
25 | vertical-align: top;
26 |
27 | &:hover {
28 | background: rgb(0, 174, 239);
29 | }
30 |
31 | svg {
32 | display: block;
33 | height: 16px;
34 | width: 16px;
35 | margin: 12px;
36 | fill: #FFF;
37 | stroke: #FFF;
38 | text-align: center;
39 | }
40 | }
41 | }
42 |
43 | .typester-pseudo-selection {
44 | background: #CCC;
45 | }
46 |
--------------------------------------------------------------------------------
/src/styles/linkDisplay.scss:
--------------------------------------------------------------------------------
1 | .typester-link-display {
2 | display: flex;
3 |
4 | a {
5 | display: block;
6 | cursor: pointer;
7 | line-height: 20px;
8 | padding: 10px;
9 | }
10 |
11 | a[href] {
12 | text-decoration: none;
13 |
14 | &:hover {
15 | text-decoration: underline;
16 | }
17 | }
18 |
19 | .typester-link-edit {
20 | display: block;
21 | height: 20px;
22 | text-align: center;
23 |
24 | svg {
25 | display: block;
26 | width: 20px;
27 | height: 20px;
28 | fill: #FFF;
29 | }
30 |
31 | &:hover {
32 | background: rgb(0, 174, 239);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/toolbar.scss:
--------------------------------------------------------------------------------
1 | .typester-toolbar {
2 | .buttons-wrapper,
3 | .inputs-wrapper {
4 | transition: opacity 200ms, transform 200ms;
5 | transform: translateY(-40px);
6 | opacity: 0;
7 |
8 | &.s--active {
9 | transform: translateY(0px);
10 | opacity: 1;
11 | }
12 | }
13 |
14 | .inputs-wrapper {
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | width: 100%;
19 | }
20 |
21 | ul {
22 | overflow: hidden;
23 | list-style: none;
24 | display: flex;
25 | padding: 0;
26 | margin: 0;
27 | }
28 |
29 | li {
30 | list-style: none;
31 | }
32 |
33 | .typester-menu-item {
34 | // transition: width 200ms;
35 | color: #FFF;
36 | font-family: sans-serif;
37 | font-size: 16px;
38 | width: 40px;
39 | height: 40px;
40 | display: block;
41 | line-height: 40px;
42 | font-weight: bold;
43 | text-align: center;
44 | cursor: pointer;
45 | user-select: none;
46 |
47 | svg {
48 | display: block;
49 | fill: #FFF;
50 | height: 16px;
51 | width: 16px;
52 | padding: 12px;
53 | }
54 |
55 | b {
56 | font-weight: bold;
57 | }
58 |
59 | i {
60 | font-style: italic;
61 | }
62 |
63 | &:hover {
64 | background: rgb(0, 174, 239);
65 | }
66 |
67 | &.s--active {
68 | background: darken(rgb(0, 174, 239), 10%);
69 | }
70 |
71 | &[disabled] {
72 | width: 0;
73 | overflow: hidden;
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/templates/flyout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{{ content }}}
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/templates/icons/link.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/src/templates/icons/orderedlist.html:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
--------------------------------------------------------------------------------
/src/templates/icons/quote.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/templates/icons/unorderedlist.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
--------------------------------------------------------------------------------
/src/templates/inputForm.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/src/templates/linkDisplay.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/src/templates/toolbar.html:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/test/integration/plugins/index.js:
--------------------------------------------------------------------------------
1 | /*global module*/
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | module.exports = (on, config) => {
16 | // `on` is used to hook into various events Cypress emits
17 | // `config` is the resolved Cypress config
18 | }
19 |
--------------------------------------------------------------------------------
/test/integration/specs/core/core_spec.js:
--------------------------------------------------------------------------------
1 | /*global cy*/
2 | describe('Core specs', function () {
3 | it('should have setup correctly', function () {
4 | cy.get('.typester-content-editable').should('be.visible').then(($el) => {
5 | expect($el).to.have.attr('contenteditable', 'true');
6 | });
7 | cy.get('.typester-toolbar').should('exist');
8 | cy.get('.typester-canvas').should('exist');
9 | });
10 |
11 | it('should destroy correctly', function () {
12 | cy.window().then(win => {
13 | win.typesterInstance.destroy();
14 | cy.get('.typester-toolbar').should('not.exist');
15 | cy.get('.typester-canvas').should('not.exist');
16 | });
17 | });
18 | });
--------------------------------------------------------------------------------
/test/integration/specs/list/list_spec.js:
--------------------------------------------------------------------------------
1 | /*global cy*/
2 | describe('List specs', function () {
3 | beforeEach(function () {
4 | cy.contentEditable().setSampleContent('paragraph');
5 | cy.toolbar().should('not.be.visible');
6 | cy.contentEditable().selectAll();
7 | });
8 |
9 | it('should create/toggle OL', function () {
10 | cy.toolbarClick('orderedlist');
11 | cy.contentEditable().assertContent('orderedList');
12 | cy.toolbarClick('orderedlist');
13 | cy.contentEditable().assertContent('paragraph', true);
14 | cy.toolbarClick('orderedlist');
15 | cy.contentEditable().assertContent('orderedList');
16 |
17 | cy.contentEditable().type('{rightarrow}{enter}');
18 | cy.contentEditable().typeSampleContent('line');
19 | cy.contentEditable().assertContent('orderedListParagraphLine');
20 | });
21 |
22 | it('should create/toggle UL', function () {
23 | cy.toolbarClick('unorderedlist');
24 | cy.contentEditable().assertContent('unorderedList');
25 | cy.toolbarClick('unorderedlist');
26 | cy.contentEditable().assertContent('paragraph', true);
27 | cy.toolbarClick('unorderedlist');
28 | cy.contentEditable().assertContent('unorderedList');
29 |
30 | cy.contentEditable().type('{rightarrow}{enter}');
31 | cy.contentEditable().typeSampleContent('line');
32 | cy.contentEditable().assertContent('unorderedListParagraphLine');
33 | });
34 |
35 | it('should create/toggle OL on new line', function () {
36 | cy.contentEditable().type('{rightarrow}{enter}');
37 | cy.contentEditable().typeSampleContent('line');
38 | cy.contentEditable().selectElement('p:last-child');
39 |
40 | cy.toolbarClick('orderedlist');
41 | cy.contentEditable().assertContent('paragraphOrderedList');
42 | cy.toolbarClick('orderedlist');
43 | cy.contentEditable().assertContent('paragraphLine');
44 | });
45 |
46 | it('should create/toggle UL on new line', function () {
47 | cy.contentEditable().type('{rightarrow}{enter}');
48 | cy.contentEditable().typeSampleContent('line');
49 | cy.contentEditable().selectElement('p:last-child');
50 |
51 | cy.toolbarClick('unorderedlist');
52 | cy.contentEditable().assertContent('paragraphUnorderedList');
53 | cy.toolbarClick('unorderedlist');
54 | cy.contentEditable().assertContent('paragraphLine');
55 | });
56 |
57 | it('should toggle off a single ordered list item', function () {
58 | cy.contentEditable().setSampleContent('orderedlist');
59 | cy.toolbar().should('not.be.visible');
60 | cy.contentEditable().selectAll();
61 |
62 | cy.contentEditable().type('{rightarrow}{enter}');
63 | cy.contentEditable().typeSampleContent('line');
64 | cy.contentEditable().assertContent('orderedListThreeItems');
65 |
66 | cy.contentEditable().selectElement('li:last-child');
67 | cy.toolbarClick('orderedlist');
68 | cy.contentEditable().assertContent('orderedListTwoItemsLine');
69 | cy.toolbarClick('h1');
70 | cy.contentEditable().assertContent('orderedListTwoItemsH1');
71 | cy.toolbarClick('orderedlist');
72 | cy.contentEditable().assertContent('orderedListThreeItems');
73 | cy.toolbarClick('orderedlist');
74 | cy.contentEditable().assertContent('orderedListTwoItemsLine');
75 | cy.toolbarClick('h2');
76 | cy.contentEditable().assertContent('orderedListTwoItemsH2');
77 | });
78 |
79 | it('should toggle off a single unordered list item', function () {
80 | cy.contentEditable().setSampleContent('unorderedlist');
81 | cy.toolbar().should('not.be.visible');
82 | cy.contentEditable().selectAll();
83 |
84 | cy.contentEditable().type('{rightarrow}{enter}');
85 | cy.contentEditable().typeSampleContent('line');
86 | cy.contentEditable().assertContent('unorderedListThreeItems');
87 |
88 | cy.contentEditable().selectElement('li:last-child');
89 | cy.toolbarClick('unorderedlist');
90 | cy.contentEditable().assertContent('unorderedListTwoItemsLine');
91 | cy.toolbarClick('h1');
92 | cy.contentEditable().assertContent('unorderedListTwoItemsH1');
93 | cy.toolbarClick('unorderedlist');
94 | cy.contentEditable().assertContent('unorderedListThreeItems');
95 | cy.toolbarClick('unorderedlist');
96 | cy.contentEditable().assertContent('unorderedListTwoItemsLine');
97 | cy.toolbarClick('h2');
98 | cy.contentEditable().assertContent('unorderedListTwoItemsH2');
99 | });
100 | });
--------------------------------------------------------------------------------
/test/integration/specs/paragraph/paragraph_spec.js:
--------------------------------------------------------------------------------
1 | /*global cy*/
2 | describe('Paragraph specs', function () {
3 | beforeEach(function () {
4 | cy.contentEditable().setSampleContent('paragraph');
5 | cy.toolbar().should('not.be.visible');
6 | cy.contentEditable().selectAll();
7 | });
8 |
9 | it('should toggle bold', function () {
10 | cy.toolbarClick('bold');
11 | cy.contentEditable().assertContent('paragraphBold');
12 | cy.toolbarClick('bold');
13 | cy.contentEditable().assertContent('paragraph', true);
14 | cy.toolbarClick('bold');
15 | cy.contentEditable().assertContent('paragraphBold');
16 | });
17 |
18 | it('should toggle italic', function () {
19 | cy.toolbarClick('italic');
20 | cy.contentEditable().assertContent('paragraphItalic');
21 | cy.toolbarClick('italic');
22 | cy.contentEditable().assertContent('paragraph', true);
23 | cy.toolbarClick('italic');
24 | cy.contentEditable().assertContent('paragraphItalic');
25 | });
26 |
27 | it('should toggle H1', function () {
28 | cy.contentEditable().selectElement('p:first-child').type('{leftarrow}{enter}');
29 | cy.contentEditable().selectElement('p:first-child').type('{leftarrow}');
30 | cy.contentEditable().typeSampleContent('line');
31 | cy.contentEditable().assertContent('lineParagraph');
32 |
33 | cy.contentEditable().selectElement('p:first-child');
34 | cy.toolbarClick('h1');
35 | cy.contentEditable().assertContent('h1Paragraph');
36 | cy.toolbarClick('h1');
37 | cy.contentEditable().assertContent('lineParagraph');
38 | cy.toolbarClick('h1');
39 | cy.contentEditable().assertContent('h1Paragraph');
40 | cy.toolbarClick('h2');
41 | cy.contentEditable().assertContent('h2Paragraph');
42 | });
43 |
44 | it('should toggle H2', function () {
45 | cy.contentEditable().selectElement('p:first-child').type('{leftarrow}{enter}');
46 | cy.contentEditable().selectElement('p:first-child').type('{leftarrow}');
47 | cy.contentEditable().typeSampleContent('line');
48 | cy.contentEditable().assertContent('lineParagraph');
49 |
50 | cy.contentEditable().selectElement('p:first-child');
51 | cy.toolbarClick('h2');
52 | cy.contentEditable().assertContent('h2Paragraph');
53 | cy.toolbarClick('h2');
54 | cy.contentEditable().assertContent('lineParagraph');
55 | cy.toolbarClick('h2');
56 | cy.contentEditable().assertContent('h2Paragraph');
57 | cy.toolbarClick('h1');
58 | cy.contentEditable().assertContent('h1Paragraph');
59 | });
60 |
61 | it('should toggle blockquote', function () {
62 | cy.toolbarClick('quote');
63 | cy.contentEditable().assertContent('paragraphBlockquote');
64 | cy.toolbarClick('quote');
65 | cy.contentEditable().assertContent('paragraph', true);
66 | cy.toolbarClick('quote');
67 | cy.contentEditable().assertContent('paragraphBlockquote');
68 | });
69 |
70 | it('should create a link', function () {
71 | cy.contentEditable().selectElement('p:first-child').type('{leftarrow}{enter}');
72 | cy.contentEditable().selectElement('p:first-child').type('{leftarrow}');
73 | cy.contentEditable().typeSampleContent('line');
74 | cy.contentEditable().assertContent('lineParagraph');
75 |
76 | cy.contentEditable().get('p:first-child').setSelection('ipsum dolor sit amet');
77 | cy.toolbarClick('link');
78 | cy.wait(100);
79 | cy.get('.typester-input-form .user-input').click().type('http://link.test{enter}');
80 | cy.contentEditable().assertContent('lineLinkParagraph');
81 | cy.toolbarClick('link');
82 | cy.contentEditable().assertContent('lineParagraph');
83 | });
84 |
85 | it('should toggle bold/italic', function () {
86 | cy.contentEditable().setSelection('iaculis mi scelerisque');
87 | cy.toolbarClick('bold');
88 | cy.contentEditable().assertContent('paragraphBoldSubstring');
89 | cy.toolbarClick('bold');
90 | cy.contentEditable().assertContent('paragraph', true);
91 | cy.toolbarClick('italic');
92 | cy.contentEditable().assertContent('paragraphItalicSubstring');
93 | cy.toolbarClick('italic');
94 | cy.contentEditable().assertContent('paragraph', true);
95 | });
96 | });
--------------------------------------------------------------------------------
/test/integration/support/commands/components.js:
--------------------------------------------------------------------------------
1 | /*global cy Cypress*/
2 | const CLASSNAMES = {
3 | contentEditable: 'typester-content-editable',
4 | toolbar: 'typester-toolbar',
5 | menuItem: 'typester-menu-item'
6 | };
7 |
8 | Cypress.Commands.add('contentEditable', function () {
9 | return cy.get(`.${CLASSNAMES.contentEditable}`);
10 | });
11 |
12 | Cypress.Commands.add('toolbar', function () {
13 | return cy.get(`.${CLASSNAMES.toolbar}`);
14 | });
15 |
16 | Cypress.Commands.add('toolbarClick', function (configKey) {
17 | return cy.get(`.${CLASSNAMES.menuItem}[data-config-key="${configKey}"]`).click({ force: true });
18 | });
--------------------------------------------------------------------------------
/test/integration/support/commands/content.js:
--------------------------------------------------------------------------------
1 | /*global cy Cypress*/
2 | Cypress.Commands.add('setContent', { prevSubject: true }, (subject, content) => {
3 | return cy.wrap(subject).then($el => {
4 | $el.html(content);
5 | });
6 | });
7 |
8 | Cypress.Commands.add('setSampleContent', { prevSubject: true }, (subject, contentKey) => {
9 | cy.fixture('sampleContent').then(sampleContent => {
10 | cy.wrap(subject).setContent(sampleContent.input[contentKey]);
11 | });
12 |
13 | return cy.wrap(subject);
14 | });
15 |
16 | Cypress.Commands.add('assertContent', { prevSubject: true }, (subject, contentKey, useInput = false) => {
17 | cy.fixture('sampleContent').then(sampleContent => {
18 | const testContent = useInput ? sampleContent.input[contentKey] : sampleContent.output[contentKey];
19 | cy.wrap(subject).should('have.html', testContent);
20 | });
21 | });
22 |
23 | Cypress.Commands.add('typeSampleContent', { prevSubject: true }, (subject, contentKey) => {
24 | cy.fixture('sampleContent').then(sampleContent => {
25 | cy.wrap(subject).type(sampleContent.input[contentKey].match(/(.*?)<\/p>/)[1]);
26 | });
27 | return cy.wrap(subject);
28 | });
--------------------------------------------------------------------------------
/test/integration/support/commands/index.js:
--------------------------------------------------------------------------------
1 | import './text-selection';
2 | import './content';
3 | import './components';
--------------------------------------------------------------------------------
/test/integration/support/commands/text-selection.js:
--------------------------------------------------------------------------------
1 | /*global cy Cypress*/
2 | /**
3 | * Credits
4 | * @Bkucera: https://github.com/cypress-io/cypress/issues/2839#issuecomment-447012818
5 | * @Phrogz: https://stackoverflow.com/a/10730777/1556245
6 | *
7 | * Usage
8 | * ```
9 | * // Types "foo" and then selects "fo"
10 | * cy.get('input')
11 | * .type('foo')
12 | * .setSelection('fo')
13 | *
14 | * // Types "foo", "bar", "baz", and "qux" on separate lines, then selects "foo", "bar", and "baz"
15 | * cy.get('textarea')
16 | * .type('foo{enter}bar{enter}baz{enter}qux{enter}')
17 | * .setSelection('foo', 'baz')
18 | *
19 | * // Types "foo" and then sets the cursor before the last letter
20 | * cy.get('input')
21 | * .type('foo')
22 | * .setCursorAfter('fo')
23 | *
24 | * // Types "foo" and then sets the cursor at the beginning of the word
25 | * cy.get('input')
26 | * .type('foo')
27 | * .setCursorBefore('foo')
28 | *
29 | * // `setSelection` can alternatively target starting and ending nodes using query strings,
30 | * // plus specific offsets. The queries are processed via `Element.querySelector`.
31 | * cy.get('body')
32 | * .setSelection({
33 | * anchorQuery: 'ul > li > p', // required
34 | * anchorOffset: 2 // default: 0
35 | * focusQuery: 'ul > li > p:last-child', // default: anchorQuery
36 | * focusOffset: 0 // default: 0
37 | * })
38 | */
39 |
40 | // Low level command reused by `setSelection` and low level command `setCursor`
41 | Cypress.Commands.add('selection', { prevSubject: true }, (subject, fn) => {
42 | cy.wrap(subject)
43 | .trigger('mousedown')
44 | .then(fn)
45 | .trigger('mouseup')
46 | .then($el => {
47 | $el.focus();
48 | cy.document().trigger('selectionchange');
49 | });
50 | return cy.wrap(subject);
51 | });
52 |
53 | Cypress.Commands.add('setSelection', { prevSubject: true }, (subject, query, endQuery) => {
54 | return cy.wrap(subject)
55 | .selection($el => {
56 | if (typeof query === 'string') {
57 | const anchorNode = getTextNode($el[0], query);
58 | const focusNode = endQuery ? getTextNode($el[0], endQuery) : anchorNode;
59 | const anchorOffset = anchorNode.wholeText.indexOf(query);
60 | const focusOffset = endQuery ?
61 | focusNode.wholeText.indexOf(endQuery) + endQuery.length :
62 | anchorOffset + query.length;
63 | setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
64 | } else if (typeof query === 'object') {
65 | const el = $el[0];
66 | const anchorNode = getTextNode(el.querySelector(query.anchorQuery));
67 | const anchorOffset = query.anchorOffset || 0;
68 | const focusNode = query.focusQuery ? getTextNode(el.querySelector(query.focusQuery)) : anchorNode;
69 | const focusOffset = query.focusOffset || 0;
70 | setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset);
71 | }
72 | });
73 | });
74 |
75 | Cypress.Commands.add('selectAll', { prevSubject: true }, (subject) => {
76 | cy.wrap(subject)
77 | .selection($el => {
78 | const rootElem = $el[0];
79 |
80 | cy.document().then((contextDocument) => {
81 | const currentSelection = contextDocument.getSelection();
82 | const range = contextDocument.createRange();
83 |
84 | range.setStart(rootElem, 0);
85 | range.setEndAfter(rootElem.lastChild);
86 |
87 | currentSelection.removeAllRanges();
88 | currentSelection.addRange(range);
89 | });
90 |
91 | return cy.wrap($el);
92 | });
93 |
94 | return cy.wrap(subject);
95 | });
96 |
97 | Cypress.Commands.add('selectElement', { prevSubject: true }, (subject, selector) => {
98 | cy.wrap(subject)
99 | .selection($el => {
100 | const rootElem = $el[0];
101 |
102 | cy.document().then(contextDocument => {
103 | const selection = contextDocument.getSelection();
104 | const newRange = new Range();
105 | const elem = rootElem.querySelector(selector);
106 |
107 | newRange.selectNode(elem);
108 | selection.removeAllRanges();
109 | selection.addRange(newRange);
110 | });
111 |
112 | return cy.wrap($el);
113 | });
114 |
115 | return cy.wrap(subject);
116 | });
117 |
118 | // Low level command reused by `setCursorBefore` and `setCursorAfter`, equal to `setCursorAfter`
119 | Cypress.Commands.add('setCursor', { prevSubject: true }, (subject, query, atStart) => {
120 | return cy.wrap(subject)
121 | .selection($el => {
122 | const node = getTextNode($el[0], query);
123 | const offset = node.wholeText.indexOf(query) + (atStart ? 0 : query.length);
124 | const document = node.ownerDocument;
125 | document.getSelection().removeAllRanges();
126 | document.getSelection().collapse(node, offset);
127 | });
128 | // Depending on what you're testing, you may need to chain a `.click()` here to ensure
129 | // further commands are picked up by whatever you're testing (this was required for Slate, for example).
130 | });
131 |
132 | Cypress.Commands.add('setCursorBefore', { prevSubject: true }, (subject, query) => {
133 | cy.wrap(subject).setCursor(query, true);
134 | });
135 |
136 | Cypress.Commands.add('setCursorAfter', { prevSubject: true }, (subject, query) => {
137 | cy.wrap(subject).setCursor(query);
138 | });
139 |
140 | // Helper functions
141 | function getTextNode(el, match) {
142 | const walk = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
143 | if (!match) {
144 | return walk.nextNode();
145 | }
146 |
147 | let node = walk.nextNode();
148 | while (node) {
149 | if (node.wholeText.includes(match)) {
150 | return node;
151 | }
152 | node = walk.nextNode();
153 | }
154 | }
155 |
156 | function setBaseAndExtent(...args) {
157 | const document = args[0].ownerDocument;
158 | document.getSelection().removeAllRanges();
159 | document.getSelection().setBaseAndExtent(...args);
160 | }
--------------------------------------------------------------------------------
/test/integration/support/index.js:
--------------------------------------------------------------------------------
1 | /*global cy*/
2 | // ***********************************************************
3 | // This example support/index.js is processed and
4 | // loaded automatically before your test files.
5 | //
6 | // This is a great place to put global configuration and
7 | // behavior that modifies Cypress.
8 | //
9 | // You can change the location of this file or turn off
10 | // automatically serving support files with the
11 | // 'supportFile' configuration option.
12 | //
13 | // You can read more here:
14 | // https://on.cypress.io/configuration
15 | // ***********************************************************
16 |
17 | // Import commands.js using ES2015 syntax:
18 | import './commands';
19 |
20 | // Alternatively you can use CommonJS syntax:
21 | // require('./commands')
22 |
23 | beforeEach(function () {
24 | // Clear the canvas
25 | cy.visit('http://localhost:9000');
26 | cy.get('.typester-content-editable').then($el => $el.html(''));
27 | });
28 |
--------------------------------------------------------------------------------
/test/server/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
60 |
61 |
62 |
63 |
73 |
74 |
75 |
76 |
77 | Quisque velit nisi, pretium ut lacinia in, elementum id enim. Donec rutrum congue leo eget malesuada. Cras ultricies ligula sed magna dictum porta. Quisque velit nisi, pretium ut lacinia in, elementum id enim. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
78 |
79 |
80 | Sed porttitor lectus nibh. Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Pellentesque in ipsum id orci porta dapibus. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Donec velit neque, auctor sit amet aliquam vel, ullamcorper sit amet ligula.
81 |
82 |
83 | Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ac diam sit amet quam vehicula elementum sed sit amet dui. Vivamus suscipit tortor eget felis porttitor volutpat. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
84 |
85 |
86 |
87 | Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Cras ultricies ligula sed magna dictum porta. Donec sollicitudin molestie malesuada. Pellentesque in ipsum id orci porta dapibus. Praesent sapien massa, convallis a pellentesque nec, egestas non nisi.
88 |
89 |
90 |
91 | Nulla quis lorem ut libero malesuada feugiat. Donec sollicitudin molestie malesuada. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem. Sed porttitor lectus nibh. Donec sollicitudin molestie malesuada.
92 |
93 |
94 | -
95 | Sed porttitor lectus nibh.
96 |
97 | -
98 | Donec rutrum congue leo eget malesuada.
99 |
100 | -
101 | Curabitur non nulla sit amet nisl tempus convallis quis ac lectus.
102 |
103 | -
104 | Vivamus suscipit tortor eget felis porttitor volutpat.
105 |
106 | -
107 | Donec sollicitudin molestie malesuada.
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
139 |
140 |
245 |
246 |
247 |
--------------------------------------------------------------------------------
/test/unit/core/Container.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Container from '../../../src/scripts/core/Container.js';
4 |
5 | describe('core/Container', () => {
6 | let container, moduleInstances, ContainerClass;
7 |
8 | const mockModule = function (id) {
9 | return function (opts) {
10 | const requestResponse = `Request response module${id}`;
11 |
12 | moduleInstances[`module${id}`] = this;
13 | opts.mediator.registerHandler('request', `request:module${id}`, () => {
14 | return requestResponse;
15 | });
16 |
17 | Object.assign(this, {
18 | name: `module${id}`,
19 | getMediator () {
20 | return opts.mediator;
21 | },
22 | doRequest (id) {
23 | return opts.mediator.request(`request:module${id}`);
24 | },
25 | getRequestResponse () {
26 | return requestResponse;
27 | }
28 | });
29 | };
30 | };
31 |
32 | const mockContainerObj = function (id) {
33 | return {
34 | name: `Container-${id}`,
35 | modules: [],
36 | containers: [],
37 | init () {}
38 | };
39 | };
40 |
41 | beforeEach(() => {
42 | let ModuleA, ModuleB, containerObj, nestedContainerObj, NestedModule;
43 |
44 | moduleInstances = {};
45 | ModuleA = mockModule('A');
46 | ModuleB = mockModule('B');
47 | containerObj = mockContainerObj('A');
48 |
49 | nestedContainerObj = mockContainerObj('Nested');
50 | NestedModule = mockModule('Nested');
51 |
52 | containerObj.modules.push({
53 | class: ModuleA,
54 | opts: {
55 | optA: true
56 | }
57 | });
58 | containerObj.modules.push({
59 | class: ModuleB,
60 | opts: {
61 | optB: true
62 | }
63 | });
64 | containerObj.containers.push({
65 | class: Container(nestedContainerObj),
66 | opts: {
67 |
68 | }
69 | });
70 | nestedContainerObj.modules.push({
71 | class: NestedModule,
72 | opts: {
73 | optNested: true
74 | }
75 | });
76 |
77 | ContainerClass = Container(containerObj);
78 | container = new ContainerClass();
79 | });
80 |
81 | xit('should be an instance of its constructor', () => {
82 | expect(container instanceof ContainerClass).toBe(true);
83 | });
84 |
85 | it('should instantiate its modules', () => {
86 | expect(moduleInstances.moduleA).toBeDefined();
87 | expect(moduleInstances.moduleB).toBeDefined();
88 | });
89 |
90 | it('should instantiate a mediator and share it with its modules', () => {
91 | const moduleAMediator = moduleInstances.moduleA.getMediator();
92 | const moduleBMediator = moduleInstances.moduleB.getMediator();
93 |
94 | expect(moduleAMediator).toBe(moduleBMediator);
95 | expect(typeof moduleAMediator.request).toBe('function');
96 | expect(typeof moduleAMediator.exec).toBe('function');
97 | expect(typeof moduleAMediator.emit).toBe('function');
98 | });
99 |
100 | it('should facilitate cross module communication through its shared mediator', () => {
101 | const moduleARequestResponse = moduleInstances.moduleA.doRequest('B');
102 | const moduleBRequestResponse = moduleInstances.moduleB.getRequestResponse();
103 |
104 | expect(moduleARequestResponse).toBe(moduleBRequestResponse);
105 | });
106 |
107 | it('should instantiate nested containers', () => {
108 | expect(moduleInstances.moduleNested).toBeDefined();
109 | });
110 |
111 | it('should facilityate cross module communication across the tree of nested containers', () => {
112 | const moduleARequestResponse = moduleInstances.moduleA.doRequest('Nested');
113 | const moduleNestedRequestResponse = moduleInstances.moduleNested.getRequestResponse();
114 |
115 | expect(moduleARequestResponse).toBe(moduleNestedRequestResponse);
116 | });
117 | });
118 |
--------------------------------------------------------------------------------
/test/unit/core/Context.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Context from '../../../src/scripts/core/Context';
4 |
5 | describe('core/Context', () => {
6 | let methodA, methodB;
7 | let contextA, contextB;
8 | let newContext;
9 |
10 | beforeEach(() => {
11 | methodA = function () {
12 | return 'methodA';
13 | };
14 | methodB = function () {
15 | return 'methodB';
16 | };
17 | contextA = {
18 | keyA: 'valA',
19 | methodA
20 | };
21 | contextB = {
22 | keyB: 'valB',
23 | methodB
24 | };
25 | newContext = new Context(contextA, contextB);
26 | });
27 |
28 | it('should create a new context from provided contexts', () => {
29 | expect(newContext.keyA).toBe(contextA.keyA);
30 | expect(newContext.keyB).toBe(contextB.keyB);
31 | expect(newContext.methodA()).toBe(contextA.methodA());
32 | expect(newContext.methodB()).toBe(contextB.methodB());
33 | });
34 |
35 | it('should be extendable', () => {
36 | const methodC = function () {
37 | return 'methodC';
38 | };
39 | const contextC = {
40 | keyC: 'valC',
41 | methodC
42 | };
43 | const contextD = {
44 | keyD: 'valD',
45 | omittedKey: 'omittedVal'
46 | };
47 |
48 | newContext.extendWith(contextC);
49 | newContext.extendWith(contextD, {keys: ['keyD']});
50 |
51 | expect(newContext.keyC).toBe(contextC.keyC);
52 | expect(newContext.methodC()).toBe(contextC.methodC());
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/test/unit/core/Mediator.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator.js';
4 |
5 | describe('core/Mediator', () => {
6 | let mediator;
7 | let requestHandlers, commandHandlers, eventHandlers, eventHandlers2;
8 | let requestResponse = 'Test request response';
9 |
10 | beforeEach(() => {
11 | mediator = new Mediator();
12 |
13 | requestHandlers = {
14 | testRequest: (suffix) => {
15 | return requestResponse + ':' + suffix;
16 | }
17 | };
18 | commandHandlers = {
19 | testCommand: () => {
20 | return null;
21 | }
22 | };
23 | eventHandlers = {
24 | testEvent: () => {
25 | return null;
26 | }
27 | };
28 | eventHandlers2 = {
29 | testEvent: () => {
30 | return null;
31 | }
32 | };
33 |
34 | spyOn(requestHandlers, 'testRequest').and.callThrough();
35 | spyOn(commandHandlers, 'testCommand').and.callThrough();
36 | spyOn(eventHandlers, 'testEvent').and.callThrough();
37 | spyOn(eventHandlers2, 'testEvent').and.callThrough();
38 | });
39 |
40 | it("should be defined", () => {
41 | expect(mediator).toBeDefined();
42 | });
43 |
44 | it("should handle request registrations", () => {
45 | mediator.registerRequestHandlers(requestHandlers);
46 | let mediatorResponse = mediator.request('testRequest', 'ping');
47 | expect(requestHandlers.testRequest).toHaveBeenCalled();
48 | expect(mediatorResponse).toEqual(requestResponse + ':ping');
49 | });
50 |
51 | it("should block duplicate request registrations", () => {
52 | mediator.registerRequestHandlers(requestHandlers);
53 | expect(() => {
54 | mediator.registerRequestHandlers(requestHandlers);
55 | }).toThrowError();
56 | });
57 |
58 | it("should handle command registrations", () => {
59 | mediator.registerCommandHandlers(commandHandlers);
60 | mediator.exec('testCommand');
61 | expect(commandHandlers.testCommand).toHaveBeenCalled();
62 | });
63 |
64 | it("should block duplicate command registrations", () => {
65 | mediator.registerCommandHandlers(commandHandlers);
66 | expect(() => {
67 | mediator.registerCommandHandlers(commandHandlers);
68 | }).toThrowError();
69 | });
70 |
71 | it("should handle event registrations", () => {
72 | mediator.registerEventHandlers(eventHandlers);
73 | mediator.registerEventHandlers(eventHandlers2);
74 | mediator.emit('testEvent');
75 | expect(eventHandlers.testEvent).toHaveBeenCalled();
76 | expect(eventHandlers2.testEvent).toHaveBeenCalled();
77 | });
78 |
79 | it("should allow for children mediators", () => {
80 |
81 | });
82 |
83 | it("should allow for a parent mediator", () => {
84 |
85 | });
86 |
87 | it("should delegate to parent if no handlers found", () => {
88 |
89 | });
90 | });
91 |
--------------------------------------------------------------------------------
/test/unit/core/Module.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 | import Mediator from '../../../src/scripts/core/Mediator.js';
3 | import Module from '../../../src/scripts/core/Module.js';
4 |
5 | import { loadFixtures } from '../helpers/fixtures.js';
6 |
7 | describe('core/Module', () => {
8 | let mediator, ModuleClass, module;
9 | let requestHandlers, commandHandlers, eventHandlers;
10 | let moduleDefinition, moduleMethods, instanceProps;
11 | let domCache, $editableEl;
12 | let moduleRequestResponse = 'Module request response';
13 |
14 | beforeEach(() => {
15 | loadFixtures();
16 |
17 | requestHandlers = {
18 | moduleRequestHandler () {
19 | return this.moduleMethod();
20 | }
21 | };
22 | commandHandlers = {
23 | moduleCommandHandler () {
24 | return null;
25 | }
26 | };
27 | eventHandlers = {
28 | moduleEventHandler () {
29 | return null;
30 | }
31 | };
32 | moduleMethods = {
33 | init () {
34 | const { dom, props } = this;
35 | domCache = dom;
36 | instanceProps = props;
37 | },
38 | moduleMethod () {
39 | return moduleRequestResponse;
40 | },
41 | domEventHandler () {}
42 | };
43 |
44 | spyOn(requestHandlers, 'moduleRequestHandler').and.callThrough();
45 | spyOn(commandHandlers, 'moduleCommandHandler').and.callThrough();
46 | spyOn(eventHandlers, 'moduleEventHandler').and.callThrough();
47 | spyOn(moduleMethods, 'moduleMethod').and.callThrough();
48 | spyOn(moduleMethods, 'init').and.callThrough();
49 | spyOn(moduleMethods, 'domEventHandler').and.callThrough();
50 |
51 | Object.assign(moduleMethods, requestHandlers, commandHandlers, eventHandlers);
52 |
53 | moduleDefinition = {
54 | name: 'testModule',
55 | props: {
56 | moduleProp: null
57 | },
58 | dom: {
59 | 'editableEl' : '.content-editable'
60 | },
61 | handlers: {
62 | requests: {
63 | 'module:request' : 'moduleRequestHandler'
64 | },
65 | commands: {
66 | 'module:command' : 'moduleCommandHandler'
67 | },
68 | events: {
69 | 'module:event' : 'moduleEventHandler'
70 | },
71 | domEvents: {
72 | 'click @editableEl' : 'domEventHandler'
73 | }
74 | },
75 | methods: moduleMethods
76 | };
77 |
78 | ModuleClass = Module(moduleDefinition);
79 | mediator = new Mediator();
80 | module = new ModuleClass({
81 | mediator,
82 | props: {
83 | moduleProp: true
84 | }
85 | });
86 |
87 | $editableEl = jQuery('.content-editable');
88 | });
89 |
90 | it('should create a module constructor', () => {
91 | expect(ModuleClass).toBeDefined();
92 | });
93 |
94 | it('should call the defined init method', () => {
95 | expect(moduleMethods.init).toHaveBeenCalled();
96 | });
97 |
98 | it('should register request handlers with the mediator', () => {
99 | let mediatorResponse = mediator.request('module:request');
100 | expect(requestHandlers.moduleRequestHandler).toHaveBeenCalled();
101 | expect(mediatorResponse).toEqual(moduleRequestResponse);
102 | });
103 |
104 | it('should register command handlers with the mediator', () => {
105 | mediator.exec('module:command');
106 | expect(commandHandlers.moduleCommandHandler).toHaveBeenCalled();
107 | });
108 |
109 | it('should register event handlers with the mediator', () => {
110 | mediator.emit('module:event');
111 | expect(eventHandlers.moduleEventHandler).toHaveBeenCalled();
112 | });
113 |
114 | it('should find and cache DOM elements', () => {
115 | expect(domCache).toBeDefined();
116 | expect(domCache.editableEl[0]).toBe($editableEl[0]);
117 | });
118 |
119 | it('should biind dom event handlers to methods', () => {
120 | $editableEl.trigger('click');
121 | expect(moduleMethods.domEventHandler).toHaveBeenCalled();
122 | });
123 |
124 | it('should merge props', () => {
125 | expect(instanceProps.moduleProp).toBe(true);
126 | });
127 |
128 | it('should ensure require props are provided', () => {
129 | const ErrorModule = Module({
130 | name: 'ErrorModule',
131 | requiredProps: ['requiredProp'],
132 | props: {
133 | requiredProp: null
134 | }
135 | });
136 |
137 | const newErrorModule = function () {
138 | new ErrorModule({ mediator });
139 | };
140 |
141 | expect(newErrorModule).toThrowError('ErrorModule requires prop: requiredProp');
142 | });
143 | });
144 |
--------------------------------------------------------------------------------
/test/unit/e2e/line.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import { e2eSetup, e2eCleanOutput, e2eClickToolbarButton } from '../helpers/e2eSetup';
4 | import e2eContent from '../helpers/e2eSampleContent';
5 | import selectionHelper from '../helpers/selection';
6 | import { loadFixtures } from '../helpers/fixtures.js';
7 |
8 | import toolbarConfig from '../../../src/scripts/config/toolbar';
9 |
10 | describe('e2e/line', function () {
11 | let mediator, editableEl, buttonConfigs;
12 | let { input, output } = e2eContent;
13 |
14 | beforeEach((done) => {
15 | loadFixtures();
16 |
17 | const setupComponents = e2eSetup();
18 | mediator = setupComponents.mediator;
19 | editableEl = setupComponents.editableEl;
20 |
21 | buttonConfigs = toolbarConfig.buttonConfigs;
22 |
23 | setTimeout(done, 250);
24 | });
25 |
26 | afterEach(() => {
27 | mediator.emit('app:destroy');
28 | mediator = null;
29 | });
30 |
31 | it('should handle multiple H1 toggling', () => {
32 | let inputContent = input.line;
33 | let outputContent = output.lineH1;
34 | let selectionString;
35 |
36 | editableEl.innerHTML = inputContent;
37 | expect(editableEl.innerHTML).toBe(inputContent);
38 |
39 | selectionHelper.selectAll(editableEl);
40 | e2eClickToolbarButton('h1');
41 | expect(e2eCleanOutput(editableEl)).toBe(outputContent);
42 | selectionString = selectionHelper.getCurrent().toString();
43 | expect(selectionString).toBe(editableEl.textContent);
44 |
45 | selectionHelper.selectAll(editableEl);
46 | e2eClickToolbarButton('h1');
47 | expect(e2eCleanOutput(editableEl)).toBe(inputContent);
48 | selectionString = selectionHelper.getCurrent().toString();
49 | expect(selectionString).toBe(editableEl.textContent);
50 |
51 | selectionHelper.selectAll(editableEl);
52 | e2eClickToolbarButton('h1');
53 | expect(e2eCleanOutput(editableEl)).toBe(outputContent);
54 | selectionString = selectionHelper.getCurrent().toString();
55 | expect(selectionString).toBe(editableEl.textContent);
56 | });
57 |
58 | it('should handle multiple H2 toggling', () => {
59 | let inputContent = input.line;
60 | let outputContent = output.lineH2;
61 | let selectionString;
62 |
63 | editableEl.innerHTML = inputContent;
64 | expect(editableEl.innerHTML).toBe(inputContent);
65 |
66 | // selectionHelper.selectAll(editableEl);
67 | // e2eClickToolbarButton('h2');
68 | // expect(e2eCleanOutput(editableEl)).toBe(outputContent);
69 | // selectionString = selectionHelper.getCurrent().toString();
70 | // expect(selectionString).toBe(editableEl.textContent);
71 | //
72 | // selectionHelper.selectAll(editableEl);
73 | // e2eClickToolbarButton('h2');
74 | // expect(e2eCleanOutput(editableEl)).toBe(inputContent);
75 | // selectionString = selectionHelper.getCurrent().toString();
76 | // expect(selectionString).toBe(editableEl.textContent);
77 | //
78 | // selectionHelper.selectAll(editableEl);
79 | // e2eClickToolbarButton('h2');
80 | // expect(e2eCleanOutput(editableEl)).toBe(outputContent);
81 | // selectionString = selectionHelper.getCurrent().toString();
82 | // expect(selectionString).toBe(editableEl.textContent);
83 | });
84 |
85 | });
86 |
--------------------------------------------------------------------------------
/test/unit/fixtures/index.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/unit/helpers/e2eSetup.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 |
5 | import UIContainer from '../../../src/scripts/containers/UIContainer';
6 | import FormatterContainer from '../../../src/scripts/containers/FormatterContainer';
7 | import CanvasContainer from '../../../src/scripts/containers/CanvasContainer';
8 |
9 | import ContentEditable from '../../../src/scripts/modules/ContentEditable';
10 | import Selection from '../../../src/scripts/modules/Selection';
11 | import Config from '../../../src/scripts/modules/Config';
12 |
13 | import mockEvents from './mockEvents';
14 |
15 | const e2eSetup = function () {
16 | let $editableEl = jQuery('.content-editable');
17 | let editableEl = $editableEl[0];
18 | let mediator = new Mediator();
19 |
20 | new ContentEditable({
21 | mediator,
22 | dom: { el: editableEl }
23 | });
24 | new Selection({
25 | mediator,
26 | dom: { el: editableEl }
27 | });
28 | new Config({ mediator });
29 |
30 | new FormatterContainer({ mediator });
31 | new UIContainer({ mediator });
32 | new CanvasContainer({ mediator });
33 |
34 | return {
35 | mediator,
36 | editableEl
37 | };
38 | };
39 |
40 | const e2eCleanOutput = function (editableEl) {
41 | let cleanOutput = '';
42 |
43 | if (!/\w+/.test(editableEl.firstChild.textContent)) {
44 | editableEl.removeChild(editableEl.firstChild);
45 | }
46 | if (!/\w+/.test(editableEl.lastChild.textContent)) {
47 | editableEl.removeChild(editableEl.lastChild);
48 | }
49 |
50 | cleanOutput = editableEl.innerHTML;
51 |
52 | // cleanOutput.match(/<(.*?)>/gi).forEach((tag) => {
53 | // cleanOutput = cleanOutput.replace(tag, tag.toLowerCase());
54 | // });
55 |
56 | return cleanOutput;
57 | };
58 |
59 | const e2eClickToolbarButton = function (configKey) {
60 | jQuery('.typester-toolbar .typester-menu-item[data-config-key="' + configKey + '"]')[0].click();
61 | };
62 |
63 | const e2eSubmitInputForm = function (userInputValue) {
64 | const $form = jQuery('.typester-input-form');
65 | const $userInput = $form.find('.user-input');
66 | $userInput.val(userInputValue);
67 | mockEvents.submit($form[0]);
68 | };
69 |
70 | const e2eFirstTextNode = function (rootElem) {
71 | let firstTextNode = rootElem.firstChild;
72 | while (firstTextNode.nodeType !== Node.TEXT_NODE) {
73 | firstTextNode = firstTextNode.firstChild;
74 | }
75 | return firstTextNode;
76 | };
77 |
78 | export default e2eSetup;
79 | export {
80 | e2eSetup,
81 | e2eCleanOutput,
82 | e2eClickToolbarButton,
83 | e2eSubmitInputForm,
84 | e2eFirstTextNode
85 | };
86 |
--------------------------------------------------------------------------------
/test/unit/helpers/fixtures.js:
--------------------------------------------------------------------------------
1 | export const loadFixtures = function () {
2 | document.body.innerHTML = '';
3 | jQuery("").appendTo(document.body);
4 | };
5 |
6 | export default {
7 | loadFixtures
8 | };
--------------------------------------------------------------------------------
/test/unit/helpers/formatterSetup.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import BaseFormatter from '../../../src/scripts/modules/BaseFormatter';
5 | import Selection from '../../../src/scripts/modules/Selection';
6 | import ContentEditable from '../../../src/scripts/modules/ContentEditable';
7 | import CanvasContainer from '../../../src/scripts/containers/CanvasContainer';
8 | import Config from '../../../src/scripts/modules/Config';
9 | import Commands from '../../../src/scripts/modules/Commands';
10 |
11 | const formatterSetup = function (Formatter, opts={}) {
12 | let $editableEl = jQuery('.content-editable');
13 | let editableEl = $editableEl[0];
14 |
15 | let mediator = new Mediator();
16 |
17 | new Selection({ mediator, dom: {el: editableEl}});
18 | new Config({ mediator });
19 | new Commands({ mediator });
20 | new ContentEditable({ mediator, dom: {el: editableEl}});
21 | new CanvasContainer({ mediator });
22 | if (!opts.skipBaseFormatter) {
23 | new BaseFormatter({ mediator });
24 | }
25 |
26 | new Formatter({ mediator });
27 |
28 | return {
29 | editableEl,
30 | mediator
31 | };
32 | };
33 |
34 | export default formatterSetup;
35 |
--------------------------------------------------------------------------------
/test/unit/helpers/mockEvents.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import keycodes from '../../../src/scripts/utils/keycodes';
4 |
5 | (function () {
6 | if (typeof window.CustomEvent === "function") {
7 | return false; //If not IE
8 | }
9 |
10 | function CustomEvent(event, params) {
11 | var evt;
12 | params = params || { bubbles: true, cancelable: true, detail: undefined };
13 | evt = document.createEvent('CustomEvent');
14 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
15 | return evt;
16 | }
17 |
18 | CustomEvent.prototype = window.Event.prototype;
19 |
20 | window.CustomEvent = CustomEvent;
21 | })();
22 |
23 | const mockEvents = {
24 | keyup (key, eventTarget=document) {
25 | let event = new CustomEvent('keyup');
26 | event.keyCode = keycodes[key];
27 | eventTarget.dispatchEvent(event);
28 | },
29 |
30 | focus (eventTarget) {
31 | let event = new CustomEvent('focus');
32 | eventTarget.dispatchEvent(event);
33 | },
34 |
35 | click (eventTarget) {
36 | let event = new CustomEvent('click', { bubbles: true });
37 | eventTarget.dispatchEvent(event);
38 | },
39 |
40 | emit (eventTarget, eventStr) {
41 | let event = new CustomEvent(eventStr, { bubbles: true });
42 | eventTarget.dispatchEvent(event);
43 | },
44 |
45 | submit (eventTarget) {
46 | let event = new CustomEvent('submit', { bubbles: true });
47 | eventTarget.dispatchEvent(event);
48 | }
49 | };
50 |
51 | export default mockEvents;
52 |
--------------------------------------------------------------------------------
/test/unit/helpers/selection.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 | import mockEvents from './mockEvents';
3 |
4 | const selectionHelper = {
5 | selectAll (elem) {
6 | const contextDocument = elem.ownerDocument;
7 | const range = contextDocument.createRange();
8 |
9 | range.selectNodeContents(elem);
10 |
11 | selectionHelper.setSelectionRange(range);
12 | },
13 |
14 | selectTextPortion (textNode, start, end) {
15 | const contextDocument = textNode.ownerDocument;
16 | const range = contextDocument.createRange();
17 |
18 | range.setStart(textNode, start);
19 | range.setEnd(textNode, end);
20 |
21 | selectionHelper.setSelectionRange(range);
22 | },
23 |
24 | selectFromTo (startNode, startOffset, endNode, endOffset) {
25 | const contextDocument = startNode.ownerDocument;
26 | const range = contextDocument.createRange();
27 |
28 | range.setStart(startNode, startOffset);
29 | range.setEnd(endNode, endOffset);
30 |
31 | selectionHelper.setSelectionRange(range);
32 | },
33 |
34 | selectFirstAndLastTextNodes (rootElem) {
35 | const rootDoc = rootElem.ownerDocument;
36 | const walker = rootDoc.createTreeWalker(
37 | rootElem,
38 | NodeFilter.SHOW_TEXT,
39 | null,
40 | false
41 | );
42 |
43 | let textNodes = [];
44 | while(walker.nextNode()) {
45 | textNodes.push(walker.currentNode);
46 | }
47 | const firstTextNode = textNodes.shift();
48 | const lastTextNode = textNodes.length ? textNodes.pop() : firstTextNode;
49 |
50 | selectionHelper.selectFromTo(firstTextNode, 0, lastTextNode, lastTextNode.textContent.length);
51 | },
52 |
53 | getCurrent (contextDocument=document) {
54 | return contextDocument.getSelection();
55 | },
56 |
57 | setSelectionRange (range) {
58 | const contextDocument = range.startContainer.ownerDocument;
59 | const contextWindow = contextDocument.defaultView || contextDocument.parentWindow;
60 | const windowSelection = contextWindow.getSelection();
61 |
62 | windowSelection.removeAllRanges();
63 | windowSelection.addRange(range);
64 |
65 | mockEvents.emit(contextDocument, 'selectionchange');
66 | },
67 |
68 | getSelectionRange () {
69 | return selectionHelper.getCurrent().getRangeAt(0);
70 | },
71 |
72 | selectNone (contextDocument=document) {
73 | const currentSelection = contextDocument.getSelection();
74 | currentSelection.removeAllRanges();
75 | mockEvents.emit(contextDocument, 'selectionchange');
76 | }
77 | };
78 |
79 | export default selectionHelper;
80 |
--------------------------------------------------------------------------------
/test/unit/helpers/userInput.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import selectionHelper from './selection';
4 | import mockEvents from './mockEvents';
5 |
6 | const userInputHelper = {
7 | focus (elem) {
8 | elem.focus();
9 | selectionHelper.selectAll(elem);
10 | mockEvents.focus(elem);
11 | }
12 | };
13 |
14 | export default userInputHelper;
15 |
--------------------------------------------------------------------------------
/test/unit/modules/BaseFormatter.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import BaseFormatter from '../../../src/scripts/modules/BaseFormatter';
5 | import zeroWidthSpace from '../../../src/scripts/utils/zeroWidthSpace';
6 |
7 | import formatterSetup from '../helpers/formatterSetup';
8 | import userInputHelper from '../helpers/userInput';
9 | import selectionHelper from '../helpers/selection';
10 | import { loadFixtures } from '../helpers/fixtures.js';
11 |
12 | describe('modules/BaseFormatter', function () {
13 | let mediator, editableEl;
14 |
15 | beforeEach((done) => {
16 | loadFixtures();
17 |
18 | const setupComponents = formatterSetup(BaseFormatter, {
19 | skipBaseFormatter: true
20 | });
21 | mediator = setupComponents.mediator;
22 | editableEl = setupComponents.editableEl;
23 |
24 | userInputHelper.focus(editableEl);
25 |
26 | setTimeout(done, 250);
27 | });
28 |
29 | afterEach(() => {
30 |
31 | });
32 |
33 | it('should copy editable content to the canvas', () => {
34 | editableEl.innerHTML = 'Basic test
';
35 | const canvasBody = mediator.get('canvas:body');
36 | selectionHelper.selectAll(editableEl);
37 | mediator.exec('format:export:to:canvas');
38 | expect(zeroWidthSpace.assert(editableEl.firstChild)).toBe(true);
39 | expect(zeroWidthSpace.assert(editableEl.lastChild)).toBe(true);
40 | editableEl.removeChild(editableEl.firstChild);
41 | editableEl.removeChild(editableEl.lastChild);
42 | expect(canvasBody.innerHTML).toBe(editableEl.innerHTML);
43 | });
44 |
45 | it('should import content from the canvas to the editable element', () => {
46 | const canvasDoc = mediator.get('canvas:document');
47 | const canvasBody = mediator.get('canvas:body');
48 | canvasBody.innerHTML = 'Basic test
';
49 | canvasBody.contentEditable = true;
50 | selectionHelper.selectAll(canvasBody.firstChild);
51 | mediator.exec('format:import:from:canvas');
52 | expect(editableEl.innerHTML).toBe(canvasBody.innerHTML);
53 | });
54 |
55 | it('should clean html on export', () => {
56 | const canvasBody = mediator.get('canvas:body');
57 | canvasBody.innerHTML = `Heading copy
58 | Sub heading copy
59 |
60 | First paragraph
61 | After first paragraph
62 | Second paragraph
63 |
64 | `;
65 | selectionHelper.selectFromTo(canvasBody.firstChild, 0, canvasBody.firstChild, 1);
66 | mediator.exec('format:import:from:canvas');
67 | if (!/\w+/.test(canvasBody.firstChild.textContent) && !/\w+/.test(editableEl.firstChild.textContent)) {
68 | canvasBody.removeChild(canvasBody.firstChild);
69 | editableEl.removeChild(editableEl.firstChild);
70 | }
71 | expect(editableEl.innerHTML).toBe('Heading copy
Sub heading copy
First paragraph
After first paragraph
Second paragraph
');
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/test/unit/modules/BlockFormatter.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import BlockFormatter from '../../../src/scripts/modules/BlockFormatter';
4 | import toolbarConfig from '../../../src/scripts/config/toolbar';
5 |
6 | import formatterSetup from '../helpers/formatterSetup';
7 | import userInputHelper from '../helpers/userInput';
8 | import selectionHelper from '../helpers/selection';
9 | import { loadFixtures } from '../helpers/fixtures.js';
10 |
11 | describe('modules/BlockFormatter', function () {
12 | let mediator;
13 | let editableEl;
14 | let headerText, buttonConfigs;
15 |
16 | headerText = 'header text';
17 |
18 | beforeEach((done) => {
19 | loadFixtures();
20 |
21 | const setupComponents = formatterSetup(BlockFormatter);
22 | editableEl = setupComponents.editableEl;
23 | mediator = setupComponents.mediator;
24 |
25 | buttonConfigs = toolbarConfig.buttonConfigs;
26 | userInputHelper.focus(editableEl);
27 |
28 | setTimeout(done, 250)
29 | });
30 |
31 | afterEach(() => {
32 | editableEl.innerHTML = '';
33 | mediator.emit('app:destroy');
34 | });
35 |
36 | xit('should default the block if contenteditable triggers newline', () => {
37 | const defaultFormatRegex = /(
)?<\/p>/;
38 | selectionHelper.selectAll(editableEl);
39 | mediator.emit('contenteditable:newline');
40 |
41 | expect(defaultFormatRegex.test(editableEl.innerHTML)).toBe(true);
42 | });
43 |
44 | it('should not try default if newline is already in a block', () => {
45 | const contentBlock = document.createElement('h1');
46 | const contentBlockText = 'text inside an existing block';
47 |
48 | contentBlock.innerHTML = contentBlockText;
49 | editableEl.innerHTML = '';
50 | editableEl.appendChild(contentBlock);
51 |
52 | selectionHelper.selectAll(contentBlock.childNodes[0]);
53 | mediator.emit('contenteditable:newline');
54 |
55 | expect(editableEl.innerHTML).toBe(`
${contentBlockText}
`);
56 | });
57 |
58 | it('should be able to format headers', () => {
59 | editableEl.innerHTML = headerText;
60 |
61 | ['H1', 'H2'/*, 'H3', 'H4', 'H5', 'H6'*/].forEach((headerStyle) => {
62 | const headerTag = headerStyle.toLowerCase();
63 | selectionHelper.selectAll(editableEl.childNodes[0]);
64 | mediator.exec('format:block', buttonConfigs[headerTag].opts);
65 | expect(editableEl.innerHTML).toBe(`<${headerTag}>${headerText}${headerTag}>`);
66 | });
67 | });
68 |
69 | it('should clear previous block formatting before performing new block format', () => {
70 | const blockOuter = document.createElement('div');
71 | const blockInner = document.createElement('div');
72 |
73 | blockInner.innerHTML = headerText;
74 | blockOuter.appendChild(blockInner);
75 | editableEl.innerHTML = '';
76 | editableEl.appendChild(blockOuter);
77 |
78 | selectionHelper.selectAll(blockInner);
79 | mediator.exec('format:block', { style: 'P', validTags: ['P'] });
80 | expect(editableEl.innerHTML).toBe(`${headerText}
`);
81 |
82 | selectionHelper.selectAll(editableEl.childNodes[0]);
83 | mediator.exec('format:block', buttonConfigs.h1.opts);
84 | expect(editableEl.innerHTML).toBe(`${headerText}
`);
85 | });
86 | });
87 |
--------------------------------------------------------------------------------
/test/unit/modules/Canvas.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import Canvas from '../../../src/scripts/modules/Canvas';
5 |
6 | import selectionHelper from '../helpers/selection';
7 | import { loadFixtures } from '../helpers/fixtures.js';
8 |
9 | describe('modules/Canvas', function () {
10 | let canvas, mediator, iframe;
11 | let $editableEl, editableEl, editableElInnerHTML;
12 |
13 | editableElInnerHTML = 'Test title
Test paragraph, with bold text
';
14 |
15 | beforeEach((done) => {
16 | loadFixtures();
17 |
18 | $editableEl = jQuery('.content-editable');
19 | editableEl = $editableEl[0];
20 |
21 | editableEl.contentEditable = true;
22 | editableEl.innerHTML = editableElInnerHTML;
23 |
24 | mediator = new Mediator();
25 | canvas = new Canvas({ mediator });
26 | iframe = document.getElementsByClassName('typester-canvas');
27 |
28 | setTimeout(done, 250);
29 | });
30 |
31 | it('should append an iframe to the document body', () => {
32 | // expect(iframe.length).toBe(1);
33 | expect(iframe.length).toBeGreaterThan(0);
34 | });
35 |
36 | it('should get the canvas document', () => {
37 | const canvasDoc = mediator.get('canvas:document');
38 | expect(canvasDoc.body).toBeDefined();
39 | });
40 |
41 | it('should get the canvas window', () => {
42 | const canvasWin = mediator.get('canvas:window');
43 | expect(canvasWin.document).toBeDefined();
44 | });
45 |
46 | it('should get the canvas body', () => {
47 | const canvasBody = mediator.get('canvas:body');
48 | expect(canvasBody.tagName).toBe('BODY');
49 | });
50 |
51 | it('should set the canvas body to be editable', () => {
52 | const canvasBody = mediator.get('canvas:body');
53 | expect(canvasBody.hasAttribute('contenteditable')).toBe(true);
54 | });
55 |
56 | it('should set the canvas content', () => {
57 | const testContentHTML = 'test content
';
58 | const canvasDoc = mediator.get('canvas:document');
59 | mediator.exec('canvas:content', testContentHTML);
60 | expect(canvasDoc.body.innerHTML).toBe(testContentHTML);
61 | });
62 | });
63 |
--------------------------------------------------------------------------------
/test/unit/modules/Commands.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import Commands from '../../../src/scripts/modules/Commands';
5 | import Config from '../../../src/scripts/modules/Config';
6 | import selectionHelper from '../helpers/selection';
7 | import DOM from '../../../src/scripts/utils/DOM';
8 | import { loadFixtures } from '../helpers/fixtures.js';
9 |
10 | describe('modules/Commands', function () {
11 | let mediator, $editableEl, editableEl;
12 |
13 | beforeEach((done) => {
14 | loadFixtures();
15 |
16 | mediator = new Mediator();
17 | new Commands({ mediator });
18 | new Config({ mediator });
19 |
20 | $editableEl = jQuery('.content-editable');
21 | editableEl = $editableEl[0];
22 | editableEl.contentEditable = true;
23 |
24 | setTimeout(done, 250);
25 | });
26 |
27 | it('should execute a given command', () => {
28 | const contentBlock = document.createElement('p');
29 | const contentBlockText = 'text inside an exisiting block';
30 |
31 | contentBlock.innerHTML = contentBlockText;
32 | editableEl.innerHTML = '';
33 | editableEl.appendChild(contentBlock);
34 |
35 | selectionHelper.selectAll(contentBlock.childNodes[0]);
36 | mediator.exec('commands:exec', {
37 | command: 'formatBlock',
38 | value: 'H1'
39 | });
40 |
41 | expect(editableEl.innerHTML).toBe(`${contentBlockText}
`);
42 | });
43 |
44 | it('should execute a default command', () => {
45 | const contentBlock = document.createElement('h1');
46 | const contentBlockText = 'text inside an exisiting block';
47 |
48 | contentBlock.innerHTML = contentBlockText;
49 | editableEl.innerHTML = '';
50 | editableEl.appendChild(contentBlock);
51 |
52 | selectionHelper.selectAll(contentBlock.childNodes[0]);
53 | mediator.exec('commands:format:default');
54 |
55 | expect(editableEl.innerHTML).toBe(`${contentBlockText}
`);
56 | });
57 |
58 | it('should execute a formatBlock command', () => {
59 | const contentBlock = document.createElement('p');
60 | const contentBlockText = 'text inside an exisiting block';
61 |
62 | contentBlock.innerHTML = contentBlockText;
63 | editableEl.innerHTML = '';
64 | editableEl.appendChild(contentBlock);
65 |
66 | selectionHelper.selectAll(contentBlock.childNodes[0]);
67 | mediator.exec('commands:format:block', {
68 | style: 'BLOCKQUOTE'
69 | });
70 |
71 |
72 | const blockquoteParagraphs = editableEl.querySelectorAll('blockquote p');
73 | blockquoteParagraphs.forEach((paragraph) => {
74 | DOM.unwrap(paragraph);
75 | });
76 |
77 | expect(editableEl.innerHTML).toBe(`${contentBlockText}
`);
78 | });
79 | });
80 |
--------------------------------------------------------------------------------
/test/unit/modules/Config.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import Config from '../../../src/scripts/modules/Config';
5 |
6 | import toolbarConfig from '../../../src/scripts/config/toolbar';
7 |
8 | describe('modules/Config', function () {
9 | let mediator;
10 | const toolbarConfigsKeysStrings = function (config, source) {
11 | const configKeysString = JSON.stringify(config.buttons.map((buttonConfig) => buttonConfig.configKey));
12 | const sourceKeysString = JSON.stringify(source);
13 | return [
14 | config.buttons.length,
15 | source.length,
16 | configKeysString,
17 | sourceKeysString
18 | ];
19 | };
20 |
21 | beforeEach(() => {
22 | mediator = new Mediator();
23 | });
24 |
25 | afterEach(() => {
26 | mediator = null;
27 | });
28 |
29 | it('should return toolbar buttons from default toolbar config', () => {
30 | new Config({ mediator });
31 | const toolbarButtons = mediator.get('config:toolbar:buttons');
32 | const [
33 | configLength,
34 | sourceLength,
35 | configKeysString,
36 | sourceKeysString
37 | ] = toolbarConfigsKeysStrings(toolbarButtons, toolbarConfig.buttons);
38 |
39 | expect(configLength).toBe(sourceLength);
40 | expect(configKeysString).toBe(sourceKeysString);
41 | });
42 |
43 | it('should return toolbar buttons from given custom config', () => {
44 | const customToolbarConfig = ['bold', 'h1', 'link'];
45 | new Config({
46 | mediator,
47 | configs: {
48 | toolbar: {
49 | buttons: customToolbarConfig
50 | }
51 | }
52 | });
53 | const toolbarButtons = mediator.get('config:toolbar:buttons');
54 | const [
55 | configLength,
56 | sourceLength,
57 | configKeysString,
58 | sourceKeysString
59 | ] = toolbarConfigsKeysStrings(toolbarButtons, customToolbarConfig);
60 |
61 | expect(configLength).toBe(sourceLength);
62 | expect(configKeysString).toBe(sourceKeysString);
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/test/unit/modules/ContentEditable.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import ContentEditable from '../../../src/scripts/modules/ContentEditable';
5 | import mockEvents from '../helpers/mockEvents';
6 | import { loadFixtures } from '../helpers/fixtures.js';
7 |
8 | describe('modules/ContentEditable', () => {
9 | let contentEditable, eventHandlers, mediator, $editableEl;
10 |
11 | beforeEach(() => {
12 | loadFixtures();
13 | mediator = new Mediator();
14 |
15 | eventHandlers = {
16 | 'contenteditable:focus': function () {},
17 | 'contenteditable:newline': function () {}
18 | };
19 | spyOn(eventHandlers, 'contenteditable:focus');
20 | spyOn(eventHandlers, 'contenteditable:newline');
21 |
22 | mediator.registerEventHandlers(eventHandlers);
23 |
24 | $editableEl = jQuery('.content-editable');
25 |
26 | contentEditable = new ContentEditable({
27 | mediator,
28 | dom: { el: $editableEl[0] }
29 | });
30 | });
31 |
32 | it('should ensure that its root element is editable', () => {
33 | expect($editableEl[0].hasAttribute('contenteditable')).toBe(true);
34 | });
35 |
36 | it('should delegate focus event', () => {
37 | $editableEl.focus();
38 | mockEvents.focus($editableEl[0]);
39 | expect(eventHandlers['contenteditable:focus']).toHaveBeenCalled();
40 | });
41 |
42 | it('should handle and delegate keyup events', (done) => {
43 | mockEvents.keyup('ENTER', $editableEl[0]);
44 | setTimeout(() => {
45 | expect(eventHandlers['contenteditable:newline']).toHaveBeenCalled();
46 | done();
47 | }, 120);
48 | }, 200);
49 |
50 | it('should handle and delegate paste events', () => {
51 |
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/test/unit/modules/Flyout.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator.js';
4 | import Flyout from '../../../src/scripts/modules/Flyout.js';
5 |
6 | describe('modules/Flyout', function () {
7 | let mediator;
8 |
9 | beforeEach(() => {
10 | mediator = new Mediator();
11 | new Flyout({ mediator });
12 | });
13 |
14 | afterEach(() => {
15 | mediator.emit('app:destroy');
16 | mediator = null;
17 | });
18 |
19 | it('should return a new dom instance of itself', () => {
20 | let flyoutDocInstance, flyout;
21 |
22 | flyout = mediator.get('flyout:new');
23 | flyout.show();
24 | flyoutDocInstance = jQuery('.typester-flyout');
25 |
26 | expect(flyout).toBeDefined();
27 | expect(flyout.el.nodeType).toBe(Node.ELEMENT_NODE);
28 | expect(flyoutDocInstance.length).toBe(1);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/unit/modules/LinkFormatter.spec.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/typecode/typester/6e81233d4046474105c2d238643fba3f00a1606e/test/unit/modules/LinkFormatter.spec.js
--------------------------------------------------------------------------------
/test/unit/modules/ListFormatter.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import ListFormatter from '../../../src/scripts/modules/ListFormatter';
4 | import toolbarConfig from '../../../src/scripts/config/toolbar';
5 |
6 | import formatterSetup from '../helpers/formatterSetup';
7 | import userInputHelper from '../helpers/userInput';
8 | import selectionHelper from '../helpers/selection';
9 | import { loadFixtures } from '../helpers/fixtures.js';
10 |
11 | describe('modules/ListFormatter', function () {
12 | let mediator;
13 | let orderedListOpts, unorderedListOpts;
14 | let editableEl;
15 |
16 | const setEditableElHTML = function () {
17 | const tmpDiv = document.createElement('div');
18 | for (let i = 0; i < 5; i++) {
19 | let pTag = document.createElement('p');
20 | pTag.innerHTML = `List item (${i})`;
21 | tmpDiv.appendChild(pTag);
22 | }
23 | editableEl.innerHTML = tmpDiv.innerHTML;
24 | userInputHelper.focus(editableEl);
25 | selectionHelper.selectFirstAndLastTextNodes(editableEl);
26 | };
27 |
28 | beforeEach((done) => {
29 | loadFixtures();
30 |
31 | const setupComponents = formatterSetup(ListFormatter);
32 | editableEl = setupComponents.editableEl;
33 | mediator = setupComponents.mediator;
34 |
35 | orderedListOpts = toolbarConfig.buttonConfigs.orderedlist.opts;
36 | unorderedListOpts = toolbarConfig.buttonConfigs.unorderedlist.opts;
37 |
38 | setTimeout(done, 250);
39 | });
40 |
41 | afterEach(() => {
42 | editableEl.innerHTML = '';
43 | mediator.emit('app:destroy');
44 | });
45 |
46 | it('should toggle ordered lists', () => {
47 | setEditableElHTML();
48 | expect(editableEl.getElementsByTagName('ol').length).toBe(0);
49 | expect(editableEl.getElementsByTagName('li').length).toBe(0);
50 |
51 | selectionHelper.selectFirstAndLastTextNodes(editableEl);
52 | mediator.exec('format:list', orderedListOpts);
53 | expect(editableEl.getElementsByTagName('ol').length).toBe(1);
54 | expect(editableEl.getElementsByTagName('li').length).toBe(5);
55 |
56 | selectionHelper.selectFirstAndLastTextNodes(editableEl);
57 | mediator.exec('format:list', orderedListOpts);
58 | expect(editableEl.getElementsByTagName('ol').length).toBe(0);
59 | expect(editableEl.getElementsByTagName('li').length).toBe(0);
60 | });
61 |
62 | it('should toggle unordered lists', () => {
63 | setEditableElHTML();
64 | expect(editableEl.getElementsByTagName('ul').length).toBe(0);
65 | expect(editableEl.getElementsByTagName('li').length).toBe(0);
66 |
67 | mediator.exec('format:list', unorderedListOpts);
68 | expect(editableEl.getElementsByTagName('ul').length).toBe(1);
69 | expect(editableEl.getElementsByTagName('li').length).toBe(5);
70 |
71 | selectionHelper.selectAll(editableEl.getElementsByTagName('ul')[0]);
72 | mediator.exec('format:list', unorderedListOpts);
73 | expect(editableEl.getElementsByTagName('ul').length).toBe(0);
74 | expect(editableEl.getElementsByTagName('li').length).toBe(0);
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/test/unit/modules/Selection.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator';
4 | import Selection from '../../../src/scripts/modules/Selection';
5 | import Config from '../../../src/scripts/modules/Config';
6 | import selectionHelper from '../helpers/selection';
7 | import { loadFixtures } from '../helpers/fixtures.js';
8 |
9 | describe('modules/Selection', () => {
10 | let $editableEl, editableEl, editableElHTML;
11 | let mediator;
12 |
13 | beforeEach(() => {
14 | loadFixtures();
15 |
16 | $editableEl = jQuery('.content-editable');
17 | editableEl = $editableEl[0];
18 | editableElHTML = 'test selection';
19 |
20 | $editableEl.html(editableElHTML);
21 | $editableEl.attr('contenteditable', true);
22 | $editableEl.focus();
23 |
24 | mediator = new Mediator();
25 | new Selection({ mediator,
26 | dom: {
27 | el: editableEl
28 | }
29 | });
30 | new Config({ mediator });
31 | });
32 |
33 | it('should return the current selection', () => {
34 | selectionHelper.selectAll(editableEl);
35 | const currentSelection = mediator.get('selection:current').toString();
36 | expect(currentSelection).toBe(editableElHTML);
37 | });
38 |
39 | it('should return the anchorNode of the current selection', () => {
40 | selectionHelper.selectTextPortion(editableEl.childNodes[0], 3, editableEl.childNodes[0].length - 3);
41 | const anchorNode = mediator.get('selection:anchornode');
42 | expect(anchorNode).toBe(editableEl.childNodes[0]);
43 | });
44 |
45 | it('should return the root element of the selection', () => {
46 | const firstDiv = document.createElement('div');
47 | const secondDiv = document.createElement('div');
48 | const thirdDiv = document.createElement('div');
49 |
50 | thirdDiv.innerHTML = 'some text';
51 | secondDiv.appendChild(thirdDiv);
52 | firstDiv.appendChild(secondDiv);
53 |
54 | editableEl.contentEditable = false;
55 | editableEl.innerHTML = '';
56 | editableEl.appendChild(firstDiv);
57 | editableEl.contentEditable = true;
58 | editableEl.focus();
59 |
60 | selectionHelper.selectAll(thirdDiv);
61 |
62 | const selectionRootEl = mediator.get('selection:rootelement');
63 | expect(selectionRootEl).toBe(editableEl);
64 | });
65 | });
66 |
--------------------------------------------------------------------------------
/test/unit/modules/TextFormatter.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import TextFormatter from '../../../src/scripts/modules/TextFormatter';
4 | import toolbarConfig from '../../../src/scripts/config/toolbar';
5 |
6 | import formatterSetup from '../helpers/formatterSetup';
7 | import userInputHelper from '../helpers/userInput';
8 | import selectionHelper from '../helpers/selection';
9 | import { loadFixtures } from '../helpers/fixtures.js';
10 |
11 | describe('modules/TextFormatter', function () {
12 | let mediator;
13 | let editableEl;
14 |
15 | beforeEach(function () {
16 | loadFixtures();
17 |
18 | const setupComponents = formatterSetup(TextFormatter);
19 | editableEl = setupComponents.editableEl;
20 | mediator = setupComponents.mediator;
21 |
22 | userInputHelper.focus(editableEl);
23 | });
24 |
25 | it('should toggle bold selection', function () {
26 | const textToBold = 'text to bold';
27 | const pTag = document.createElement('p');
28 | const config = toolbarConfig.buttonConfigs.bold;
29 | let textToBoldIndex;
30 | let isBold = false;
31 | let unBolded = false;
32 |
33 | pTag.innerHTML = 'Some text to bold!';
34 | editableEl.innerHTML = '';
35 |
36 | editableEl.appendChild(pTag);
37 | textToBoldIndex = pTag.innerHTML.indexOf(textToBold);
38 | selectionHelper.selectTextPortion(pTag.childNodes[0], textToBoldIndex, textToBoldIndex + textToBold.length);
39 |
40 | mediator.exec('format:text', config.opts);
41 | isBold = editableEl.innerHTML === 'Some text to bold!
' || editableEl.innerHTML === 'Some text to bold!
';
42 | expect(isBold).toBe(true);
43 |
44 | selectionHelper.selectAll(pTag.childNodes[1]);
45 | mediator.exec('format:text', config.opts);
46 | unBolded = editableEl.innerHTML === 'Some text to bold!
';
47 | expect(unBolded).toBe(true);
48 | });
49 | });
50 |
--------------------------------------------------------------------------------
/test/unit/modules/Toolbar.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import Mediator from '../../../src/scripts/core/Mediator.js';
4 | import Toolbar from '../../../src/scripts/modules/Toolbar.js';
5 | import Selection from '../../../src/scripts/modules/Selection.js';
6 | import Flyout from '../../../src/scripts/modules/Flyout.js';
7 | import Config from '../../../src/scripts/modules/Config.js';
8 |
9 | import mockEvents from '../helpers/mockEvents';
10 | import selectionHelper from '../helpers/selection';
11 | import { loadFixtures } from '../helpers/fixtures.js';
12 |
13 | describe('modules/Toolbar', function () {
14 | let mediator, commands;
15 | let toolbarEl, elStyle, editableEl, flyoutEl;
16 |
17 | beforeEach(() => {
18 | loadFixtures();
19 |
20 | editableEl = document.getElementsByClassName('content-editable')[0];
21 |
22 | mediator = new Mediator();
23 | commands = {
24 | 'format:block' : () => {}
25 | };
26 | spyOn(commands, 'format:block').and.callThrough();
27 | mediator.registerCommandHandlers(commands);
28 |
29 | new Selection({ mediator,
30 | dom: { el: editableEl },
31 | props: { contextDocument: document }
32 | });
33 | new Config({ mediator });
34 | new Flyout({ mediator });
35 | new Toolbar({ mediator, opts: {
36 | dom: {
37 | el: document.body
38 | }
39 | }});
40 |
41 | flyoutEl = document.getElementsByClassName('typester-flyout');
42 | toolbarEl = document.getElementsByClassName('typester-toolbar');
43 |
44 | editableEl.innerHTML = 'Test text
';
45 | editableEl.contentEditable = true;
46 |
47 | selectionHelper.selectAll(editableEl.childNodes[0]);
48 |
49 | jasmine.clock().install();
50 | });
51 |
52 | afterEach(() => {
53 | mediator.emit('app:destroy');
54 | mediator = null;
55 | jasmine.clock().uninstall();
56 | });
57 |
58 | it('should append styles', () => {
59 | const stylesEl = document.getElementById('typester-styles');
60 | expect(stylesEl).toBeDefined();
61 | });
62 |
63 | it('should inject its template', () => {
64 | expect(flyoutEl.length).toBe(1);
65 | expect(toolbarEl.length).toBe(1);
66 | selectionHelper.selectNone();
67 | mediator.emit('selection:change');
68 | expect(flyoutEl[0].style.display).toBe('none');
69 | });
70 |
71 | it('should handle toolbar clicks', () => {
72 | const menuItems = document.getElementsByClassName('typester-menu-item');
73 | selectionHelper.selectAll(editableEl.childNodes[0]);
74 |
75 | mockEvents.click(menuItems[2]);
76 | expect(commands['format:block']).toHaveBeenCalledWith({ style: 'H1', validTags: ['H1'], toggle: false });
77 | mockEvents.click(menuItems[3]);
78 | expect(commands['format:block']).toHaveBeenCalledWith({ style: 'H2', validTags: ['H2'], toggle: false });
79 | });
80 |
81 | it('should show when a range has been selected', () => {
82 | editableEl.focus();
83 | selectionHelper.selectAll(editableEl.childNodes[0]);
84 | mediator.emit('selection:change');
85 | jasmine.clock().tick(100);
86 | expect(flyoutEl[0].style.display).toBe('block');
87 | });
88 |
89 | it('should handle document selectstart', () => {
90 | // NB expect('test').toBe('written');
91 | });
92 |
93 | it('should handle document selectionchange', () => {
94 | // NB expect('test').toBe('written');
95 | });
96 | });
97 |
--------------------------------------------------------------------------------
/test/unit/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | case "$1" in
4 |
5 | test)
6 | xvfb-run -l npm run test 2> /dev/null || :
7 | ;;
8 |
9 | test_ci)
10 | xvfb-run -l npm run test_ci
11 | ;;
12 |
13 | *)
14 | echo $"Usage: $0 {test|test_ci}"
15 | exit 1
16 | esac
--------------------------------------------------------------------------------
/test/unit/setup.js:
--------------------------------------------------------------------------------
1 | import '../../src/scripts/polyfills';
2 |
--------------------------------------------------------------------------------
/test/unit/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "**/*[sS]pec.js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.js"
8 | ],
9 | "stopSpecOnExpectationFailure": false,
10 | "random": false
11 | }
12 |
--------------------------------------------------------------------------------
/test/unit/tmp/insertHTML.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | xdescribe('insertHTML', function () {
4 | let editableDiv;
5 |
6 | beforeEach(() => {
7 | editableDiv = document.createElement('div');
8 | document.body.appendChild(editableDiv);
9 | editableDiv.contentEditable = true;
10 | editableDiv.focus();
11 | });
12 |
13 | it('should detect if enabled', () => {
14 | expect(document.activeElement).toBe(editableDiv);
15 | expect(document.execCommand('insertHTML', null, '123
')).toBe(true);
16 | expect(editableDiv.innerHTML).toBe('123
');
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/test/unit/utils/func.spec.js:
--------------------------------------------------------------------------------
1 | // jshint strict: false
2 |
3 | import func from '../../../src/scripts/utils/func';
4 |
5 | describe('utils/func', () => {
6 | let testFunc, testContext;
7 |
8 | beforeEach(() => {
9 | testFunc = function () {
10 | return this;
11 | };
12 | testContext = {
13 | context: 'test'
14 | };
15 | });
16 |
17 | it('should bind a function to a context', function () {
18 | const boundFunc = func.bind(testFunc, testContext);
19 | expect(boundFunc()).toBe(testContext);
20 | });
21 |
22 | it('should bind an object of functions to a context', function () {
23 | const boundFuncObj = func.bindObj({testFunc}, testContext);
24 | expect(boundFuncObj.testFunc()).toBe(testContext);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/test/unit/utils/guid.spec.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/typecode/typester/6e81233d4046474105c2d238643fba3f00a1606e/test/unit/utils/guid.spec.js
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const path = require('path');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | mode: process.env.BUILD || "development", // "production" | "development" | "none"
8 |
9 | entry: './src/scripts/index.js',
10 |
11 | output: {
12 | path: path.resolve(__dirname, 'build/js/'),
13 | filename: process.env.BUILD === 'production' ? 'typester.min.js' : 'typester.js',
14 | library: 'Typester',
15 | libraryExport: "default",
16 | libraryTarget: 'umd'
17 | },
18 |
19 | resolve: {
20 | extensions: ['.js', '.html']
21 | },
22 |
23 | module: {
24 | rules: [
25 | {
26 | test: /\.js/,
27 | loader: 'babel-loader'
28 | },
29 | {
30 | test: /\.html/,
31 | loader: 'handlebars-loader'
32 | },
33 | {
34 | test: /\.scss/,
35 | use: [
36 | 'style-loader',
37 | 'css-loader',
38 | 'sass-loader'
39 | ]
40 | }
41 | ]
42 | },
43 |
44 | plugins: [
45 | new webpack.ProvidePlugin({
46 | Typester: ['Typester', 'default']
47 | })
48 | ],
49 |
50 | devServer: {
51 | contentBase: [path.resolve(__dirname, 'build/'), path.resolve(__dirname, 'test/server/')],
52 | host: '0.0.0.0',
53 | port: 9000,
54 | disableHostCheck: true,
55 | index: 'index.html',
56 | publicPath: '/js/'
57 | }
58 | }
--------------------------------------------------------------------------------