├── .babelrc ├── .eslintrc ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml └── workflows │ └── close_pull_requests.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Assets ├── img │ └── grapesjsbuilder.png └── library │ ├── css │ └── grapes-code-editor.min.css │ └── js │ ├── asset.service.js │ ├── builder.js │ ├── builder.service.js │ ├── codeMode │ ├── codeEditor.js │ ├── codeMode.button.js │ └── codeMode.command.js │ ├── dist │ ├── builder.css │ ├── builder.css.map │ ├── builder.js │ ├── builder.js.map │ ├── grapesjs-preset-webpage.min.b28e95f2.css │ ├── grapesjs-preset-webpage.min.b28e95f2.css.map │ ├── main-fonts.064bcdb7.eot │ ├── main-fonts.bdf12d61.woff │ ├── main-fonts.d5e9f6d2.svg │ └── main-fonts.e77e32f4.ttf │ └── grapesjs-custom.css ├── Config └── config.php ├── Controller ├── FileManagerController.php └── GrapesJsController.php ├── Demo ├── data │ ├── response.template.blank-mjml.json │ └── response.template.blank.json ├── helloWorld │ ├── helloWorld.js │ ├── index.html │ └── style.css └── mautic │ ├── full.html │ └── index.html ├── Entity ├── GrapesJsBuilder.php └── GrapesJsBuilderRepository.php ├── EventSubscriber ├── AssetsSubscriber.php ├── EmailSubscriber.php └── InjectCustomContentSubscriber.php ├── GrapesJsBuilderBundle.php ├── Helper └── FileManager.php ├── Integration ├── Config.php ├── GrapesJsBuilderIntegration.php └── Support │ ├── BuilderSupport.php │ └── ConfigSupport.php ├── Model └── GrapesJsBuilderModel.php ├── README.md ├── Tests └── Unit │ └── Model │ └── GrapesJsBuilderModelTest.php ├── Translations ├── cs │ └── javascript.ini ├── de │ └── javascript.ini ├── en_US │ └── javascript.ini ├── fr │ └── javascript.ini └── pt_BR │ └── javascript.ini ├── Views ├── Builder │ └── template.html.php └── Setting │ ├── fields.html.php │ └── vars.html.php ├── composer.json ├── easy-coding-standard.yml ├── package-lock.json ├── package.json ├── phpstan.neon └── phpunit.xml /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "extends": ["airbnb/base","prettier"], 6 | "parser": "babel-eslint", 7 | "plugins": ["prettier"], 8 | "rules": { 9 | "prettier/prettier": ["error"], 10 | "no-restricted-syntax": 0 11 | }, 12 | "ignorePatterns": ["Assets/library/js/dist","node_modules","*.min.js"], 13 | "globals": { 14 | "mQuery": true, 15 | // "grapesjs": true, 16 | "Mousetrap":true, 17 | "Mautic":true, 18 | "mauticAjaxCsrf":true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | The primary source of the Code of Conduct is at [mautic.org](https://www.mautic.org/code-of-conduct/) - it is reproduced here for reference. 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of the Mautic community is to support you and your business in the development, use and implementation of Mautic. It’s to be inclusive and add value to the largest number of participants, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all. 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, whether in-person or online, as well as the consequences for unacceptable behavior. 8 | 9 | Your participation is contingent upon following these guidelines in all Mautic activities, including but not limited to: 10 | 11 | * Using Mautic community resources. 12 | * Working with other Mauticians and other Mautic community participants whether virtually or co-located. 13 | * Representing Mautic at public events. 14 | * Representing Mautic in social media (official accounts, personal accounts, Facebook pages and groups). 15 | * Participating in Mautic sprints and training events. 16 | * Participating in Mautic-related forums, mailing lists, wikis, websites, chat channels, bugs, group or person-to-person meetings, and Mautic-related correspondence. 17 | 18 | We invite all those who participate in Mautic activities online to help us create safe and positive experiences for everyone, everywhere. 19 | 20 | 21 | ## 2. Open Source & Culture Citizenship 22 | 23 | A supplemental goal of this Code of Conduct is to increase open source and culture citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 24 | 25 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 26 | 27 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, please recognize their efforts. 28 | 29 | ## 3. Welcoming to all 30 | We are committed to providing a friendly, safe and welcoming environment for all, regardless of level of experience or job role, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, national origin, citizenship and immigration status, neurodiversity, mental health or socio-economic status. 31 | 32 | 33 | ## 4. Expected Behavior 34 | 35 | The following behaviors are expected and requested of all community members: 36 | 37 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 38 | * Exercise consideration and respect in your speech and actions. 39 | * Attempt collaboration before conflict. 40 | * Guide conversations toward issue resolution. 41 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 42 | 43 | Alert Mautic team members if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 44 | 45 | ## 5. Unacceptable Behavior 46 | 47 | The following behaviors are considered harassment and are unacceptable within our community: 48 | 49 | * **Violence and Threats of Violence** are not acceptable - online or offline. This includes incitement of violence toward any individual, including encouraging a person to commit self-harm. This also includes posting or threatening to post other people’s personally identifying information (“doxxing”) online. 50 | * **Public or private harassment** is never acceptable in any form. 51 | * **Personal Attacks** Conflicts will inevitably arise, but frustration should never turn into a personal attack. It is not okay to insult, demean or belittle others. Attacking someone for their opinions, beliefs and ideas is not acceptable. It is important to speak directly when we disagree and when we think we need to improve, but such discussions must be conducted respectfully and professionally, remaining focused on the issue at hand. 52 | * **Derogatory Language** Hurtful or harmful language is never acceptable in any context related to: background, family status, gender, gender identity or expression, marital status, sex, sexual orientation, personal appearance, body size, native language, age, ability, neurodiversity, mental health, race and/or ethnicity, national origin, citizenship and immigration status, socioeconomic status, religion, geographic location. 53 | * **Unwelcome Sexual Attention or Physical Contact** Unwelcome sexual attention or unwelcome physical contact is not acceptable. This includes sexualized comments, jokes or imagery in interactions, communications or presentation materials, as well as inappropriate touching, groping, or sexual advances. This includes touching a person without permission, including sensitive areas such as their hair, pregnant stomach, mobility device (wheelchair, scooter, etc) or tattoos. This also includes physically blocking or intimidating another person. Physical contact or simulated physical contact (such as emojis like “kiss”) without affirmative consent is not acceptable. This includes sharing or distribution of sexualized images or text. 54 | * **Disruptive Behavior** Sustained disruption of events, forums, or meetings, including talks and presentations, will not be tolerated. This includes spamming community discussions with the solicitation of unwanted products or services. 55 | * **Influencing Disruptive Behavior** We will treat influencing or leading such activities the same way we treat the activities themselves, and thus the same consequences apply. 56 | 57 | ## 6. Consequences of Unacceptable Behavior 58 | 59 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 60 | 61 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 62 | 63 | If a community member engages in unacceptable behavior, we may take any action deemed appropriate, up to and including a temporary ban or permanent expulsion from the community without warning. Examples of sanctions which may be applied include but is not limited to: 64 | * Verbal warnings. 65 | * Written warnings. 66 | * Temporary absence from participation. 67 | * Long-term absence from participation. 68 | * Being required to follow a conduct agreement that dictates the process of returning to the community. 69 | 70 | 71 | ## 7. Reporting Guidelines 72 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify us as soon as possible by emailing info@mautic.org, or contacting a Mautic team member on the specific platform. 73 | 74 | Processes for dealing with breaches of the Code of Conduct can be found [here][coc-breaches]. 75 | 76 | ## 8. Addressing Grievances 77 | Only permanent resolutions (such as bans) may be appealed. To appeal a decision, contact the Mautic team at info@mautic.org with your appeal and the team will review the situation. 78 | 79 | ## 9. Scope 80 | We expect all community participants (contributors, moderators and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community affairs. 81 | 82 | While this code of conduct is specifically aimed at Mautic’s official resources and community, we recognize that it is possible for actions taken outside of Mautic’s official online or in person spaces to have a deep impact on community health. 83 | 84 | Resources or incidents which break this code of conduct for any reason in a non-Mautic community location will be considered in the same way as resources or incidents from owned channels, and subject to the same sanctions. 85 | 86 | ## 10. Contact info 87 | For more information, please contact info@mautic.org. 88 | 89 | ## 11. License and attribution 90 | This Code of Conduct is directly adapted from the Stumptown Syndicate and distributed under a [Creative Commons Attribution-ShareAlike license][cc-by-sa]. 91 | 92 | Additional text from [Mozilla Community Participation Guidelines][mozilla-guidelines] distributed under a [Creative Commons Attribution-ShareAlike license][cc-by-sa]. 93 | 94 | Reviewed and updated using the [Mozilla Code of Conduct Assessment Tool][mozilla-tool]. 95 | 96 | [coc-breaches]: 97 | [mozilla-guidelines]: 98 | [cc-by-sa]: 99 | [mozilla-tool]: 100 | 101 | (Code of Conduct is subject to change without notice). 102 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Reporting Security Vulnerabilities 2 | 3 | If you think that you have found a security vulnerability, please email security@mautic.com with as much detail as possible. The core team will review the vulnerability and if found applicable, will create the patch in a private repository. The vulnerability will be disclosed once the patch has been included into a release. 4 | 5 | ## Contributing Code 6 | 7 | Development is open and available to any member of the Mautic community. All fixes and improvements are done through pull requests to the code. This code is open source and publicly available. 8 | 9 | ### Developer Documentation 10 | 11 | Developer documentation is available at [https://developer.mautic.org](https://developer.mautic.org). To add additions or corrects to the documentation, submit Issues or Pull Requests against [https://github.com/mautic/developer-documentation](https://github.com/mautic/developer-documentation). 12 | 13 | ### Core Feature Development Procedures 14 | 15 | Pull Requests with additional features should be created with the Mautic Core goals in consideration. Any features that are created for core that don’t follow the overall goals may not be included. 16 | 17 | In addition to following the general direction of the development goals, the pull request code must be well-formed following coding standards and guidelines. If you wish to target a specific release version number for the feature, its best to make the pull request early so any feedback from the core team can be implemented and adequate testing can be performed. 18 | 19 | Features that are determined not to fit within the direction of the Mautic Core goals are more than welcome to be created as plugins instead. 20 | 21 | ### Code Contribution Requirements 22 | 23 | #### Code Standards 24 | 25 | Mautic follows [Symfony's coding standards](http://symfony.com/doc/current/contributing/code/standards.html) by implementing pre-commit git hook running [php-cs-fixer](https://github.com/friendsofphp/php-cs-fixer), which is installed and updated with `composer install`/`composer update`. 26 | 27 | All code styling is handled automatically by the aforementioned git hook. In case if you setup git hook correctly (which is true if you ever run `composer install`/`composer update` before creating a pull request), you can format your code as you like - it will be converted to Mautic code style automatically. 28 | 29 | #### Automated Tests 30 | 31 | All code contributions should include adequate and appropriate unit tests using [PHPUnit](https://phpunit.de/manual/5.7/en/index.html) and/or Symfony functional tests ([https://symfony.com/doc/2.8/testing.html](https://symfony.com/doc/2.8/testing.html)). Pull Requests without these tests will not be merged. 32 | 33 | #### Pull Request Description 34 | 35 | When creating a new Pull Request, the description template should be filled appropriately in detail. Any Pull Request that does not have an appropriate description will not be considered for merge. 36 | 37 | #### Documentation 38 | 39 | Each new feature should include a reference to a pull request in our [End User Documentation](https://github.com/mautic/documentation) repository or [Developer Documentation](https://github.com/mautic/developer-documentation) repository if applicable. 40 | 41 | ## Core Development Rules 42 | 43 | Pull requests and code submissions are decided upon by the release leader and the core team. When a decision is not clearly evident then the following voting process will be implemented. 44 | 45 | ### Voting Policy 46 | 47 | Votes are cast by all members of the core team. Votes can be changed at any time during the discussion. Positive votes require no explanation. A negative vote must be justified by technical or objective logic. A core team member cannot vote on any code they submit. 48 | 49 | ### Merging Policy 50 | 51 | The voting process on any particular pull request must allow for enough time for review by the community and the core team. This involves a minimum of 2 days for minor modifications and minimum of 5 days for significant code changes. Minor changes involve typographical errors, documentation, code standards, minor CSS, javascript, and HTML modifications. Minor modifications do not require a voting process. All other submissions require a vote after the minimum code review period and must be approved by two or more core members (with no core members voting against). 52 | 53 | ### Core Membership Application 54 | 55 | Core team members are based on a form of meritocracy. We actively seek to empower our active community members and those demonstrating increased involvement will be given everything needed for their continued success. 56 | 57 | ### Core Membership Revocation 58 | 59 | A Mautic Core membership can be revoked for any of the following reasons: 60 | 61 | - Refusal to follow the rules and policies listed herein 62 | - Lack of activity for the previous 6 months 63 | - Willful negligence or intent to harm the Mautic project 64 | - Upon decision of the project leader 65 | 66 | Revoked members may re-apply for core membership following a 12 month period. 67 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: mautic 2 | open_collective: mautic 3 | -------------------------------------------------------------------------------- /.github/workflows/close_pull_requests.yml: -------------------------------------------------------------------------------- 1 | # Workflow name: 2 | name: Close Pull Requests 3 | 4 | # Workflow triggers: 5 | on: 6 | pull_request_target: 7 | types: [opened] 8 | 9 | # Workflow jobs: 10 | jobs: 11 | run: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: superbrothers/close-pull-request@v3 15 | with: 16 | comment: | 17 | Thank you for submitting a pull request. :raised_hands: 18 | 19 | We greatly appreciate your willingness to submit a contribution. However, we are not accepting pull requests against this repository, as all development happens on the [main project repository](https://github.com/mautic/mautic). 20 | 21 | We kindly request that you submit this pull request against the [respective directory](https://github.com/mautic/mautic/blob/head/plugins/GrapesJsBuilderBundle) of the main repository where we'll review and provide feedback. If this is your first Mautic contribution, be sure to read the [contributing guide](https://github.com/mautic/mautic/blob/4.x/.github/CONTRIBUTING.md) which provides guidelines and instructions for submitting contributions. 22 | 23 | Thank you again, and we look forward to receiving your contribution! :smiley: 24 | 25 | Best, 26 | The Mautic team -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/* 2 | node_modules/* -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | Assets/library/js/dist 2 | *.min.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /Assets/img/grapesjsbuilder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mautic/plugin-grapesjs-builder/6997b9ad266ec709cb16a60ef3a291bafe79a55a/Assets/img/grapesjsbuilder.png -------------------------------------------------------------------------------- /Assets/library/css/grapes-code-editor.min.css: -------------------------------------------------------------------------------- 1 | .code-panel{text-align:left;font-size:1rem;height:100%;display:flex;flex-direction:column}.code-panel section{flex:1}.code-panel section .codepanel-separator{display:flex;justify-content:space-between;padding-left:.6rem;padding-right:.6rem}.code-panel section .codepanel-label{line-height:20px;font-size:13px;color:#aaa;user-select:none;text-transform:uppercase}.code-panel section button{background-color:#d6d6d6}.gutter{cursor:ns-resize;position:relative;background-color:rgba(0,0,0,0.2)}.gutter:after{content:'';display:block;height:8px;width:100%;position:absolute;top:-3px;z-index:150}.code-panel .CodeMirror{height:calc(100% - 20px)}.gjs-pn-views{border-left:1px solid rgba(0,0,0,0.2);border-bottom:0}.gjs-pn-views-container{box-shadow:initial;top:40px;padding-top:0;height:calc(100% - 40px)}.gjs-pn-views-container,.gjs-cv-canvas{transition:width .3s ease-in-out} 2 | -------------------------------------------------------------------------------- /Assets/library/js/asset.service.js: -------------------------------------------------------------------------------- 1 | export default class AssetService { 2 | 3 | /** 4 | * Get a list of all existing assets (e.g. images) 5 | * to display in the assets manager, and the config 6 | * 7 | * @returns array 8 | */ 9 | static getAssets() { 10 | const textareaAssets = mQuery('textarea#grapesjsbuilder_assets'); 11 | const files = textareaAssets.val() ? JSON.parse(textareaAssets.val()) : []; 12 | const uploadPath = textareaAssets.data('upload'); 13 | const deletePath = textareaAssets.data('delete'); 14 | return { 15 | files, 16 | conf: { 17 | uploadPath, 18 | deletePath, 19 | }, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Assets/library/js/builder.js: -------------------------------------------------------------------------------- 1 | import AssetService from './asset.service'; 2 | import BuilderService from './builder.service'; 3 | // import grapesjsmautic from 'grapesjs-preset-mautic/src/content.service'; 4 | 5 | // all css get combined into one builder.css and automatically loaded via js/parcel 6 | import 'grapesjs/dist/css/grapes.min.css'; 7 | // not compatible with the newsletter preset css, brings the redish color 8 | // import 'grapesjs-preset-webpage/dist/grapesjs-preset-webpage.min.css'; 9 | import 'grapesjs-preset-newsletter/dist/grapesjs-preset-newsletter.css'; 10 | import './grapesjs-custom.css'; 11 | 12 | /** 13 | * Launch builder 14 | * 15 | * @param formName 16 | * @param actionName 17 | */ 18 | function launchBuilderGrapesjs(formName) { 19 | const assets = AssetService.getAssets(); 20 | 21 | const builder = new BuilderService(assets); 22 | 23 | Mautic.showChangeThemeWarning = true; 24 | 25 | // Prepare HTML 26 | mQuery('html').css('font-size', '100%'); 27 | mQuery('body').css('overflow-y', 'hidden'); 28 | mQuery('.builder-panel').css('padding', 0); 29 | mQuery('.builder-panel').css('display', 'block'); 30 | mQuery('.builder').addClass('builder-active').removeClass('hide'); 31 | 32 | // Initialize GrapesJS 33 | builder.initGrapesJS(formName); 34 | } 35 | 36 | /** 37 | * Set theme's HTML 38 | * 39 | * @param theme 40 | */ 41 | function setThemeHtml(theme) { 42 | BuilderService.setupButtonLoadingIndicator(true); 43 | // Load template and fill field 44 | mQuery.ajax({ 45 | url: mQuery('#builder_url').val(), 46 | data: `template=${theme}`, 47 | dataType: 'json', 48 | success(response) { 49 | const textareaHtml = mQuery('textarea.builder-html'); 50 | const textareaMjml = mQuery('textarea.builder-mjml'); 51 | 52 | textareaHtml.val(response.templateHtml); 53 | 54 | if (typeof textareaMjml !== 'undefined') { 55 | textareaMjml.val(response.templateMjml); 56 | } 57 | 58 | // If MJML template, generate HTML before save 59 | // if (!textareaHtml.val().length && textareaMjml.val().length) { 60 | // builder.mjmlToHtml(textareaMjml, textareaHtml); 61 | // } 62 | // } 63 | }, 64 | error(request, textStatus) { 65 | console.log(`setThemeHtml - Request failed: ${textStatus}`); 66 | }, 67 | complete() { 68 | BuilderService.setupButtonLoadingIndicator(false); 69 | }, 70 | }); 71 | } 72 | 73 | /** 74 | * The builder button to launch GrapesJS will be disabled when the code mode theme is selected 75 | * 76 | * @param theme 77 | */ 78 | function switchBuilderButton(theme) { 79 | const builderButton = mQuery('.btn-builder'); 80 | const mEmailBuilderButton = mQuery('#emailform_buttons_builder_toolbar_mobile'); 81 | const mPageBuilderButton = mQuery('#page_buttons_builder_toolbar_mobile'); 82 | const isCodeMode = theme === 'mautic_code_mode'; 83 | 84 | builderButton.attr('disabled', isCodeMode); 85 | 86 | if (isCodeMode) { 87 | mPageBuilderButton.addClass('link-is-disabled'); 88 | mEmailBuilderButton.addClass('link-is-disabled'); 89 | 90 | mPageBuilderButton.parent().addClass('is-not-allowed'); 91 | mEmailBuilderButton.parent().addClass('is-not-allowed'); 92 | } else { 93 | mPageBuilderButton.removeClass('link-is-disabled'); 94 | mEmailBuilderButton.removeClass('link-is-disabled'); 95 | 96 | mPageBuilderButton.parent().removeClass('is-not-allowed'); 97 | mEmailBuilderButton.parent().removeClass('is-not-allowed'); 98 | } 99 | } 100 | 101 | /** 102 | * The textarea with the HTML source will be displayed if the code mode theme is selected 103 | * 104 | * @param theme 105 | */ 106 | function switchCustomHtml(theme) { 107 | const customHtmlRow = mQuery('#custom-html-row'); 108 | const isPageMode = mQuery('[name="page"]').length !== 0; 109 | const isCodeMode = theme === 'mautic_code_mode'; 110 | const advancedTab = isPageMode ? mQuery('#advanced-tab') : null; 111 | 112 | if (isCodeMode === true) { 113 | customHtmlRow.removeClass('hidden'); 114 | isPageMode && advancedTab.removeClass('hidden'); 115 | } else { 116 | customHtmlRow.addClass('hidden'); 117 | isPageMode && advancedTab.addClass('hidden'); 118 | } 119 | } 120 | 121 | /** 122 | * Initialize original Mautic theme selection with grapejs specific modifications 123 | */ 124 | function initSelectThemeGrapesjs(parentInitSelectTheme) { 125 | function childInitSelectTheme(themeField) { 126 | const builderUrl = mQuery('#builder_url'); 127 | let url; 128 | 129 | switchBuilderButton(themeField.val()); 130 | switchCustomHtml(themeField.val()); 131 | 132 | // Replace Mautic URL by plugin URL 133 | if (builderUrl.length) { 134 | if (builderUrl.val().indexOf('pages') !== -1) { 135 | url = builderUrl.val().replace('s/pages/builder', 's/grapesjsbuilder/page'); 136 | } else { 137 | url = builderUrl.val().replace('s/emails/builder', 's/grapesjsbuilder/email'); 138 | } 139 | 140 | builderUrl.val(url); 141 | } 142 | 143 | // Launch original Mautic.initSelectTheme function 144 | parentInitSelectTheme(themeField); 145 | 146 | mQuery('[data-theme]').click((event) => { 147 | const theme = mQuery(event.target).attr('data-theme'); 148 | 149 | switchBuilderButton(theme); 150 | switchCustomHtml(theme); 151 | }); 152 | } 153 | return childInitSelectTheme; 154 | } 155 | 156 | Mautic.launchBuilder = launchBuilderGrapesjs; 157 | Mautic.initSelectTheme = initSelectThemeGrapesjs(Mautic.initSelectTheme); 158 | Mautic.setThemeHtml = setThemeHtml; 159 | -------------------------------------------------------------------------------- /Assets/library/js/builder.service.js: -------------------------------------------------------------------------------- 1 | import grapesjs from 'grapesjs'; 2 | import grapesjsmjml from 'grapesjs-mjml'; 3 | import grapesjsnewsletter from 'grapesjs-preset-newsletter'; 4 | import grapesjswebpage from 'grapesjs-preset-webpage'; 5 | import grapesjspostcss from 'grapesjs-parser-postcss'; 6 | import contentService from 'grapesjs-preset-mautic/dist/content.service'; 7 | import grapesjsmautic from 'grapesjs-preset-mautic'; 8 | import mjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service'; 9 | import 'grapesjs-plugin-ckeditor'; 10 | 11 | // for local dev 12 | // import contentService from '../../../../../../grapesjs-preset-mautic/src/content.service'; 13 | // import grapesjsmautic from '../../../../../../grapesjs-preset-mautic/src'; 14 | // import mjmlService from '../../../../../../grapesjs-preset-mautic/src/mjml/mjml.service'; 15 | 16 | import CodeModeButton from './codeMode/codeMode.button'; 17 | 18 | export default class BuilderService { 19 | editor; 20 | 21 | assets; 22 | 23 | uploadPath; 24 | 25 | deletePath; 26 | 27 | /** 28 | * @param {*} assets 29 | */ 30 | constructor(assets) { 31 | if (!assets.conf.uploadPath) { 32 | throw Error('No uploadPath found'); 33 | } 34 | if (!assets.conf.deletePath) { 35 | throw Error('No deletePath found'); 36 | } 37 | if (!assets.files || !assets.files[0]) { 38 | console.warn('no assets'); 39 | } 40 | 41 | this.assets = assets.files; 42 | this.uploadPath = assets.conf.uploadPath; 43 | this.deletePath = assets.conf.deletePath; 44 | } 45 | 46 | /** 47 | * Initialize GrapesJsBuilder 48 | * 49 | * @param object 50 | */ 51 | setListeners() { 52 | if (!this.editor) { 53 | throw Error('No editor found'); 54 | } 55 | 56 | // Why would we not want to keep the history? 57 | // 58 | // this.editor.on('load', () => { 59 | // const um = this.editor.UndoManager; 60 | // // Clear stack of undo/redo 61 | // um.clear(); 62 | // }); 63 | 64 | const keymaps = this.editor.Keymaps; 65 | let allKeymaps; 66 | 67 | this.editor.on('modal:open', () => { 68 | // Save all keyboard shortcuts 69 | allKeymaps = { ...keymaps.getAll() }; 70 | 71 | // Remove keyboard shortcuts to prevent launch behind popup 72 | keymaps.removeAll(); 73 | }); 74 | 75 | this.editor.on('modal:close', () => { 76 | // ReMap keyboard shortcuts on modal close 77 | Object.keys(allKeymaps).map((objectKey) => { 78 | const shortcut = allKeymaps[objectKey]; 79 | 80 | keymaps.add(shortcut.id, shortcut.keys, shortcut.handler); 81 | return keymaps; 82 | }); 83 | }); 84 | 85 | this.editor.on('asset:remove', (response) => { 86 | // Delete file on server 87 | mQuery.ajax({ 88 | url: this.deletePath, 89 | data: { filename: response.getFilename() }, 90 | }); 91 | }); 92 | } 93 | 94 | /** 95 | * Initialize the grapesjs build in the 96 | * correct mode 97 | */ 98 | initGrapesJS(object) { 99 | // disable mautic global shortcuts 100 | Mousetrap.reset(); 101 | if (object === 'page') { 102 | this.editor = this.initPage(); 103 | } else if (object === 'emailform') { 104 | if (mjmlService.getOriginalContentMjml()) { 105 | this.editor = this.initEmailMjml(); 106 | } else { 107 | this.editor = this.initEmailHtml(); 108 | } 109 | } else { 110 | throw Error(`Not supported builder type: ${object}`); 111 | } 112 | 113 | // add code mode button 114 | // @todo: only show button if configured: sourceEdit: 1, 115 | const codeModeButton = new CodeModeButton(this.editor); 116 | codeModeButton.addCommand(); 117 | codeModeButton.addButton(); 118 | 119 | this.setListeners(); 120 | } 121 | 122 | static getMauticConf(mode) { 123 | return { 124 | mode, 125 | }; 126 | } 127 | 128 | static getCkeConf() { 129 | return { 130 | options: { 131 | language: 'en', 132 | toolbar: [ 133 | { name: 'links', items: ['Link', 'Unlink'] }, 134 | { name: 'basicstyles', items: ['Bold', 'Italic', 'Strike', '-', 'RemoveFormat'] }, 135 | { name: 'paragraph', items: ['NumberedList', 'BulletedList', '-'] }, 136 | { name: 'colors', items: ['TextColor', 'BGColor'] }, 137 | { name: 'document', items: ['Source'] }, 138 | { name: 'insert', items: ['SpecialChar'] }, 139 | ], 140 | extraPlugins: ['sharedspace', 'colorbutton'], 141 | }, 142 | }; 143 | } 144 | 145 | /** 146 | * Initialize the builder in the landingapge mode 147 | */ 148 | initPage() { 149 | // Launch GrapesJS with body part 150 | this.editor = grapesjs.init({ 151 | clearOnRender: true, 152 | container: '.builder-panel', 153 | components: contentService.getOriginalContentHtml().body.innerHTML, 154 | height: '100%', 155 | canvas: { 156 | styles: contentService.getStyles(), 157 | }, 158 | storageManager: false, // https://grapesjs.com/docs/modules/Storage.html#basic-configuration 159 | assetManager: this.getAssetManagerConf(), 160 | styleManager: { 161 | clearProperties: true, // Temp fix https://github.com/artf/grapesjs-preset-webpage/issues/27 162 | }, 163 | plugins: [grapesjswebpage, grapesjspostcss, grapesjsmautic, 'gjs-plugin-ckeditor'], 164 | pluginsOpts: { 165 | [grapesjswebpage]: { 166 | formsOpts: false, 167 | }, 168 | grapesjsmautic: BuilderService.getMauticConf('page-html'), 169 | 'gjs-plugin-ckeditor': BuilderService.getCkeConf(), 170 | }, 171 | }); 172 | 173 | return this.editor; 174 | } 175 | 176 | initEmailMjml() { 177 | const components = mjmlService.getOriginalContentMjml(); 178 | // validate 179 | mjmlService.mjmlToHtml(components); 180 | 181 | this.editor = grapesjs.init({ 182 | clearOnRender: true, 183 | container: '.builder-panel', 184 | components, 185 | height: '100%', 186 | storageManager: false, 187 | assetManager: this.getAssetManagerConf(), 188 | plugins: [grapesjsmjml, grapesjspostcss, grapesjsmautic, 'gjs-plugin-ckeditor'], 189 | pluginsOpts: { 190 | grapesjsmjml: {}, 191 | grapesjsmautic: BuilderService.getMauticConf('email-mjml'), 192 | 'gjs-plugin-ckeditor': BuilderService.getCkeConf(), 193 | }, 194 | }); 195 | 196 | this.editor.BlockManager.get('mj-button').set({ 197 | content: 'Button', 198 | }); 199 | 200 | return this.editor; 201 | } 202 | 203 | initEmailHtml() { 204 | const components = contentService.getOriginalContentHtml().body.innerHTML; 205 | if (!components) { 206 | throw new Error('no components'); 207 | } 208 | 209 | // Launch GrapesJS with body part 210 | this.editor = grapesjs.init({ 211 | clearOnRender: true, 212 | container: '.builder-panel', 213 | components, 214 | height: '100%', 215 | storageManager: false, 216 | assetManager: this.getAssetManagerConf(), 217 | plugins: [grapesjsnewsletter, grapesjspostcss, grapesjsmautic, 'gjs-plugin-ckeditor'], 218 | pluginsOpts: { 219 | grapesjsnewsletter: {}, 220 | grapesjsmautic: BuilderService.getMauticConf('email-html'), 221 | 'gjs-plugin-ckeditor': BuilderService.getCkeConf(), 222 | }, 223 | }); 224 | 225 | // add a Mautic custom block Button 226 | this.editor.BlockManager.get('button').set({ 227 | content: 228 | '\n' + 229 | 'Button\n' + 230 | '', 231 | }); 232 | 233 | return this.editor; 234 | } 235 | 236 | /** 237 | * Manage button loading indicator 238 | * 239 | * @param activate - true or false 240 | */ 241 | static setupButtonLoadingIndicator(activate) { 242 | const builderButton = mQuery('.btn-builder'); 243 | const saveButton = mQuery('.btn-save'); 244 | const applyButton = mQuery('.btn-apply'); 245 | 246 | if (activate) { 247 | Mautic.activateButtonLoadingIndicator(builderButton); 248 | Mautic.activateButtonLoadingIndicator(saveButton); 249 | Mautic.activateButtonLoadingIndicator(applyButton); 250 | } else { 251 | Mautic.removeButtonLoadingIndicator(builderButton); 252 | Mautic.removeButtonLoadingIndicator(saveButton); 253 | Mautic.removeButtonLoadingIndicator(applyButton); 254 | } 255 | } 256 | 257 | /** 258 | * Configure the Asset Manager for all modes 259 | * @link https://grapesjs.com/docs/modules/Assets.html#configuration 260 | */ 261 | getAssetManagerConf() { 262 | return { 263 | assets: this.assets, 264 | noAssets: Mautic.translate('grapesjsbuilder.assetManager.noAssets'), 265 | upload: this.uploadPath, 266 | uploadName: 'files', 267 | multiUpload: 1, 268 | embedAsBase64: false, 269 | openAssetsOnDrop: 1, 270 | autoAdd: 1, 271 | headers: { 'X-CSRF-Token': mauticAjaxCsrf }, // global variable 272 | }; 273 | } 274 | 275 | getEditor() { 276 | return this.editor; 277 | } 278 | /** 279 | * Generate assets list from GrapesJs 280 | */ 281 | // getAssetsList() { 282 | // const assetManager = this.editor.AssetManager; 283 | // const assets = assetManager.getAll(); 284 | // const assetsList = []; 285 | 286 | // assets.forEach((asset) => { 287 | // if (asset.get('type') === 'image') { 288 | // assetsList.push({ 289 | // src: asset.get('src'), 290 | // width: asset.get('width'), 291 | // height: asset.get('height'), 292 | // }); 293 | // } else { 294 | // assetsList.push(asset.get('src')); 295 | // } 296 | // }); 297 | 298 | // return assetsList; 299 | // } 300 | } 301 | -------------------------------------------------------------------------------- /Assets/library/js/codeMode/codeEditor.js: -------------------------------------------------------------------------------- 1 | // import ContentService from '../../../../../../../grapesjs-preset-mautic/src/content.service'; 2 | import MjmlService from 'grapesjs-preset-mautic/dist/mjml/mjml.service'; 3 | import ContentService from 'grapesjs-preset-mautic/dist/content.service'; 4 | 5 | class CodeEditor { 6 | editor; 7 | 8 | opts; 9 | 10 | codeEditor; 11 | 12 | codePopup; 13 | 14 | constructor(editor, opts = {}) { 15 | this.editor = editor; 16 | this.opts = opts; 17 | 18 | this.codeEditor = this.buildCodeEditor(); 19 | this.codePopup = this.buildCodePopup(); 20 | } 21 | 22 | // Build codeEditor (CodeMirror instance) 23 | buildCodeEditor() { 24 | const codeEditor = this.editor.CodeManager.getViewer('CodeMirror').clone(); 25 | 26 | codeEditor.set({ 27 | codeName: 'htmlmixed', 28 | readOnly: false, 29 | theme: 'hopscotch', 30 | autoBeautify: true, 31 | autoCloseTags: true, 32 | autoCloseBrackets: true, 33 | lineWrapping: true, 34 | styleActiveLine: true, 35 | smartIndent: true, 36 | indentWithTabs: true, 37 | }); 38 | 39 | return codeEditor; 40 | } 41 | 42 | // Build popup content, codeEditor area and buttons 43 | buildCodePopup() { 44 | const cfg = this.editor.getConfig(); 45 | 46 | const codePopup = document.createElement('div'); 47 | const btnEdit = document.createElement('button'); 48 | const btnCancel = document.createElement('button'); 49 | const textarea = document.createElement('textarea'); 50 | 51 | btnEdit.innerHTML = Mautic.translate('grapesjsbuilder.sourceEditBtnLabel'); 52 | btnEdit.className = `${cfg.stylePrefix}btn-prim ${cfg.stylePrefix}btn-code-edit`; 53 | btnEdit.onclick = this.updateCode.bind(this); 54 | 55 | btnCancel.innerHTML = Mautic.translate('grapesjsbuilder.sourceCancelBtnLabel'); 56 | btnCancel.className = `${cfg.stylePrefix}btn-prim ${cfg.stylePrefix}btn-code-cancel`; 57 | btnCancel.onclick = this.cancelCode.bind(this); 58 | 59 | codePopup.appendChild(textarea); 60 | codePopup.appendChild(btnEdit); 61 | codePopup.appendChild(btnCancel); 62 | 63 | this.codeEditor.init(textarea); 64 | 65 | return codePopup; 66 | } 67 | 68 | // Load content and show popup 69 | showCodePopup(editor) { 70 | this.updateEditorContents(); 71 | // this.codeEditor.editor.refresh(); 72 | // editor.Modal.setContent(''); 73 | editor.Modal.setContent(this.codePopup); 74 | editor.Modal.setTitle(Mautic.translate('grapesjsbuilder.sourceEditModalTitle')); 75 | editor.Modal.open(); 76 | 77 | editor.Modal.onceClose(() => editor.stopCommand('preset-mautic:code-edit')); 78 | } 79 | 80 | /** 81 | * Update the main editors canvas content with the 82 | * content from modals editor. 83 | * @todo show validation results in UI 84 | */ 85 | updateCode() { 86 | const code = this.codeEditor.editor.getValue(); 87 | // validate MJML code 88 | if (ContentService.isMjmlMode(this.editor)) { 89 | MjmlService.mjmlToHtml(code); 90 | } 91 | 92 | try { 93 | // delete canvas and set new content 94 | this.editor.DomComponents.getWrapper().set('content', ''); 95 | this.editor.setComponents(code.trim()); 96 | this.editor.Modal.close(); 97 | } catch (e) { 98 | window.alert(`${Mautic.translate('grapesjsbuilder.sourceSyntaxError')} \n${e.message}`); 99 | } 100 | } 101 | 102 | // Close popup 103 | cancelCode() { 104 | this.editor.Modal.close(); 105 | } 106 | 107 | /** 108 | * Set the content to be edited in the popup editor 109 | */ 110 | updateEditorContents() { 111 | // Check if MJML plugin is on 112 | let content; 113 | if (ContentService.isMjmlMode(this.editor)) { 114 | content = MjmlService.getEditorMjmlContent(this.editor); 115 | } else { 116 | content = ContentService.getEditorHtmlContent(this.editor); 117 | } 118 | this.codeEditor.setContent(content); 119 | } 120 | } 121 | 122 | export default CodeEditor; 123 | -------------------------------------------------------------------------------- /Assets/library/js/codeMode/codeMode.button.js: -------------------------------------------------------------------------------- 1 | import CodeModeCommand from './codeMode.command'; 2 | 3 | export default class CodeModeButton { 4 | editor; 5 | 6 | /** 7 | * Add close button with save for Mautic 8 | */ 9 | constructor(editor) { 10 | if (!editor) { 11 | throw new Error('no editor'); 12 | } 13 | this.editor = editor; 14 | } 15 | 16 | addButton() { 17 | this.editor.Panels.addButton('options', [ 18 | { 19 | id: 'code-edit', 20 | className: 'fa fa-edit', 21 | attributes: { 22 | title: Mautic.translate('grapesjsbuilder.sourceEditModalTitle'), 23 | }, 24 | command: CodeModeCommand.name, 25 | }, 26 | ]); 27 | } 28 | 29 | addCommand() { 30 | this.editor.Commands.add(CodeModeCommand.name, { 31 | run: CodeModeCommand.launchCodeEditorModal, 32 | stop: CodeModeCommand.stopCodeEditorModal, 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Assets/library/js/codeMode/codeMode.command.js: -------------------------------------------------------------------------------- 1 | import CodeEditor from './codeEditor'; 2 | 3 | export default class CodeModeCommand { 4 | /** 5 | * The command to run on button click 6 | */ 7 | static name = 'preset-mautic:code-edit'; 8 | 9 | codeEditor; 10 | 11 | static launchCodeEditorModal(editor, sender, opts) { 12 | if (!editor) { 13 | throw new Error('no editor'); 14 | } 15 | 16 | if (!CodeModeCommand.codeEditor) { 17 | CodeModeCommand.codeEditor = new CodeEditor(editor, opts); 18 | } 19 | 20 | if (sender) { 21 | sender.set('active', 0); 22 | } 23 | 24 | CodeModeCommand.codeEditor.showCodePopup(editor); 25 | 26 | // Transform DC Component to token 27 | editor.runCommand('preset-mautic:dynamic-content-components-to-tokens'); 28 | } 29 | 30 | static stopCodeEditorModal(editor) { 31 | if (!editor) { 32 | throw new Error('no editor'); 33 | } 34 | // Transform Token to Components 35 | editor.runCommand('preset-mautic:update-dc-components-from-dc-store'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Assets/library/js/dist/grapesjs-preset-webpage.min.b28e95f2.css: -------------------------------------------------------------------------------- 1 | .gjs-one-bg{background-color:#463a3c}.gjs-one-color,.gjs-one-color-h:hover{color:#463a3c}.gjs-two-bg{background-color:#b9a5a6}.gjs-two-color,.gjs-two-color-h:hover{color:#b9a5a6}.gjs-three-bg{background-color:#804f7b}.gjs-three-color,.gjs-three-color-h:hover{color:#804f7b}.gjs-four-bg{background-color:#d97aa6}.gjs-four-color,.gjs-four-color-h:hover{color:#d97aa6} 2 | /*# sourceMappingURL=grapesjs-preset-webpage.min.b28e95f2.css.map */ -------------------------------------------------------------------------------- /Assets/library/js/dist/grapesjs-preset-webpage.min.b28e95f2.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["grapesjs-preset-webpage.min.css"],"names":[],"mappings":"AAAA,YAAY,wBAAwB,CAA8B,sCAAuB,aAAa,CAAC,YAAY,wBAAwB,CAA8B,sCAAuB,aAAa,CAAC,cAAc,wBAAwB,CAAgC,0CAAyB,aAAa,CAAC,aAAa,wBAAwB,CAA+B,wCAAwB,aAAa","file":"grapesjs-preset-webpage.min.b28e95f2.css","sourceRoot":"..","sourcesContent":[".gjs-one-bg{background-color:#463a3c}.gjs-one-color{color:#463a3c}.gjs-one-color-h:hover{color:#463a3c}.gjs-two-bg{background-color:#b9a5a6}.gjs-two-color{color:#b9a5a6}.gjs-two-color-h:hover{color:#b9a5a6}.gjs-three-bg{background-color:#804f7b}.gjs-three-color{color:#804f7b}.gjs-three-color-h:hover{color:#804f7b}.gjs-four-bg{background-color:#d97aa6}.gjs-four-color{color:#d97aa6}.gjs-four-color-h:hover{color:#d97aa6}\n"]} -------------------------------------------------------------------------------- /Assets/library/js/dist/main-fonts.064bcdb7.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mautic/plugin-grapesjs-builder/6997b9ad266ec709cb16a60ef3a291bafe79a55a/Assets/library/js/dist/main-fonts.064bcdb7.eot -------------------------------------------------------------------------------- /Assets/library/js/dist/main-fonts.bdf12d61.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mautic/plugin-grapesjs-builder/6997b9ad266ec709cb16a60ef3a291bafe79a55a/Assets/library/js/dist/main-fonts.bdf12d61.woff -------------------------------------------------------------------------------- /Assets/library/js/dist/main-fonts.d5e9f6d2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 34 | 35 | 42 | 51 | 55 | 58 | 63 | 68 | 73 | 78 | 83 | 88 | 93 | 98 | 103 | 108 | 113 | 118 | 123 | 128 | 133 | 138 | 143 | 148 | 153 | 154 | 155 | 186 | 196 | 197 | 199 | 200 | 202 | image/svg+xml 203 | 205 | 206 | 207 | 208 | 209 | 214 | 219 | 224 | 229 | 234 | 240 | 245 | 248 | 253 | 254 | 269 | 285 | 290 | Borders: 30pxCanvas: 1000x1000px 308 | 312 | 315 | 317 | 321 | 325 | 329 | 333 | 337 | 341 | 345 | 349 | 350 | 351 | 352 | 353 | 358 | 363 | 368 | 373 | 378 | 383 | 388 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /Assets/library/js/dist/main-fonts.e77e32f4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mautic/plugin-grapesjs-builder/6997b9ad266ec709cb16a60ef3a291bafe79a55a/Assets/library/js/dist/main-fonts.e77e32f4.ttf -------------------------------------------------------------------------------- /Assets/library/js/grapesjs-custom.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | margin: 0; 4 | height: 100%; 5 | } 6 | 7 | /* GrapesJS temporary fix #2490 */ 8 | .gjs-clm-tag-status, 9 | .gjs-clm-tag-close { 10 | } 11 | .gjs-clm-tags-btn { 12 | width: 24px; 13 | } 14 | 15 | /* Fix hidden scroll */ 16 | .gjs-pn-views-container { 17 | height: auto; 18 | padding: 0 0 0; 19 | top: 40px; 20 | bottom: 0; 21 | background: #f0f0f0; 22 | } 23 | 24 | .gjs-btn-code-edit { 25 | margin-top: 10px; 26 | } 27 | 28 | /* GrapesJS FileManager custom CSS */ 29 | .gjs-btn-code-cancel { 30 | margin-left: 10px; 31 | } 32 | 33 | .gjs-am-file-uploader { 34 | width: 100%; 35 | float: none; 36 | } 37 | 38 | .gjs-am-assets-cont { 39 | width: 100%; 40 | float: none; 41 | } 42 | 43 | .gjs-am-assets { 44 | height: 280px; 45 | } 46 | 47 | .gjs-am-asset { 48 | width: 25%; 49 | } 50 | 51 | .gjs-am-file-uploader > form #gjs-am-uploadFile { 52 | padding: 20px 10px; 53 | } 54 | 55 | .gjs-am-file-uploader #gjs-am-title { 56 | padding: 20px 10px; 57 | } 58 | 59 | .gjs-am-preview { 60 | background-size: contain; 61 | } 62 | 63 | .gjs-am-preview-cont { 64 | width: 40%; 65 | } 66 | 67 | .gjs-am-meta { 68 | width: 60%; 69 | } 70 | 71 | .gjs-pn-commands .gjs-pn-buttons { 72 | justify-content: center; 73 | } 74 | 75 | /* GrapesJS Colors / Theme */ 76 | .gjs-clm-tags .gjs-sm-title, 77 | .gjs-sm-sector .gjs-sm-title { 78 | } 79 | 80 | .gjs-clm-tags .gjs-clm-tag { 81 | border: 1px solid #707070; 82 | box-shadow: none; 83 | text-shadow: none; 84 | } 85 | 86 | .gjs-field { 87 | box-shadow: none; 88 | } 89 | 90 | .gjs-btnt.gjs-pn-active, 91 | .gjs-pn-btn.gjs-pn-active { 92 | box-shadow: none; 93 | } 94 | 95 | .gjs-import-label, 96 | .gjs-export-label { 97 | margin-bottom: 10px; 98 | font-size: 13px; 99 | } 100 | 101 | .gjs-mdl-dialog .gjs-btn-import { 102 | margin-top: 10px; 103 | } 104 | 105 | .CodeMirror { 106 | border-radius: 3px; 107 | height: 450px; 108 | font-family: sans-serif, monospace; 109 | letter-spacing: 0.3px; 110 | font-size: 12px; 111 | } 112 | 113 | /* GrapesJS Extra */ 114 | #gjs-pn-views-container.gjs-pn-panel { 115 | padding: 39px 0 0; 116 | } 117 | 118 | #gjs-pn-views.gjs-pn-panel { 119 | padding: 0; 120 | border: none; 121 | } 122 | 123 | #gjs-pn-views .gjs-pn-btn { 124 | margin: 0; 125 | height: 40px; 126 | padding: 10px; 127 | width: 25%; 128 | border-bottom: 2px solid rgba(0, 0, 0, 0.3); 129 | } 130 | 131 | #gjs-pn-views .gjs-pn-active { 132 | border-radius: 0; 133 | } 134 | 135 | #gjs-pn-devices-c { 136 | padding-left: 30px; 137 | } 138 | 139 | #gjs-pn-options { 140 | padding-right: 30px; 141 | } 142 | 143 | .gjs-sm-composite .gjs-sm-properties { 144 | display: flex; 145 | flex-flow: row wrap; 146 | justify-content: space-between; 147 | } 148 | 149 | #gjs-sm-border-top-left-radius, 150 | #gjs-sm-border-top-right-radius, 151 | #gjs-sm-border-bottom-left-radius, 152 | #gjs-sm-border-bottom-right-radius, 153 | #gjs-sm-margin-top, 154 | #gjs-sm-margin-bottom, 155 | #gjs-sm-margin-right, 156 | #gjs-sm-margin-left, 157 | #gjs-sm-padding-top, 158 | #gjs-sm-padding-bottom, 159 | #gjs-sm-padding-right, 160 | #gjs-sm-padding-left { 161 | } 162 | 163 | #gjs-sm-border-width, 164 | #gjs-sm-border-style, 165 | #gjs-sm-border-color { 166 | flex: 999 1 80px; 167 | } 168 | 169 | #gjs-sm-margin-left, 170 | #gjs-sm-padding-left { 171 | order: 2; 172 | } 173 | 174 | #gjs-sm-margin-right, 175 | #gjs-sm-padding-right { 176 | order: 3; 177 | } 178 | 179 | #gjs-sm-margin-bottom, 180 | #gjs-sm-padding-bottom { 181 | order: 4; 182 | } 183 | 184 | .gjs-field-radio { 185 | width: 100%; 186 | } 187 | 188 | .gjs-field-radio #gjs-sm-input-holder { 189 | display: flex; 190 | } 191 | 192 | .gjs-radio-item { 193 | flex: 1 0 auto; 194 | text-align: center; 195 | } 196 | 197 | .gjs-sm-sector .gjs-sm-property.gjs-sm-list { 198 | width: 50%; 199 | } 200 | 201 | .gjs-mdl-content { 202 | border-top: none; 203 | } 204 | 205 | .gjs-sm-sector .gjs-sm-property .gjs-sm-layer.gjs-sm-active { 206 | background-color: rgba(255, 255, 255, 0.09); 207 | } 208 | 209 | .gjs-f-button::before { 210 | content: 'B'; 211 | } 212 | 213 | .gjs-f-divider::before { 214 | content: 'D'; 215 | } 216 | 217 | .gjs-mdl-dialog-sm { 218 | width: 300px; 219 | } 220 | 221 | .gjs-mdl-dialog form .gjs-sm-property { 222 | font-size: 12px; 223 | margin-bottom: 15px; 224 | } 225 | 226 | .gjs-mdl-dialog form .gjs-sm-label { 227 | margin-bottom: 5px; 228 | } 229 | 230 | #gjs-clm-status-c { 231 | display: none; 232 | } 233 | 234 | .anim-spin { 235 | animation: 0.5s linear 0s normal none infinite running spin; 236 | } 237 | 238 | .form-status { 239 | float: right; 240 | font-size: 14px; 241 | } 242 | 243 | .text-danger { 244 | color: #f92929; 245 | } 246 | 247 | @keyframes spin { 248 | 0% { 249 | transform: rotate(0deg); 250 | } 251 | 100% { 252 | transform: rotate(360deg); 253 | } 254 | } 255 | 256 | .is-not-allowed { 257 | cursor: not-allowed; 258 | } 259 | 260 | .link-is-disabled { 261 | pointer-events: none; 262 | text-decoration: none; 263 | display: inline-block; 264 | opacity: 0.5; 265 | } 266 | -------------------------------------------------------------------------------- /Config/config.php: -------------------------------------------------------------------------------- 1 | 'GrapesJS Builder', 7 | 'description' => 'GrapesJS Builder with MJML support for Mautic', 8 | 'version' => '1.0.0', 9 | 'author' => 'Mautic Community', 10 | 'routes' => [ 11 | 'main' => [ 12 | 'grapesjsbuilder_upload' => [ 13 | 'path' => '/grapesjsbuilder/upload', 14 | 'controller' => 'GrapesJsBuilderBundle:FileManager:upload', 15 | ], 16 | 'grapesjsbuilder_delete' => [ 17 | 'path' => '/grapesjsbuilder/delete', 18 | 'controller' => 'GrapesJsBuilderBundle:FileManager:delete', 19 | ], 20 | 'grapesjsbuilder_builder' => [ 21 | 'path' => '/grapesjsbuilder/{objectType}/{objectId}', 22 | 'controller' => 'GrapesJsBuilderBundle:GrapesJs:builder', 23 | ], 24 | ], 25 | 'public' => [], 26 | 'api' => [], 27 | ], 28 | 'menu' => [], 29 | 'services' => [ 30 | 'other' => [ 31 | // Provides access to configured API keys, settings, field mapping, etc 32 | 'grapesjsbuilder.config' => [ 33 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\Integration\Config::class, 34 | 'arguments' => [ 35 | 'mautic.integrations.helper', 36 | ], 37 | ], 38 | ], 39 | 'sync' => [], 40 | 'integrations' => [ 41 | // Basic definitions with name, display name and icon 42 | 'mautic.integration.grapesjsbuilder' => [ 43 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\Integration\GrapesJsBuilderIntegration::class, 44 | 'tags' => [ 45 | 'mautic.integration', 46 | 'mautic.basic_integration', 47 | ], 48 | ], 49 | // Provides the form types to use for the configuration UI 50 | 'grapesjsbuilder.integration.configuration' => [ 51 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\Integration\Support\ConfigSupport::class, 52 | 'tags' => [ 53 | 'mautic.config_integration', 54 | ], 55 | ], 56 | // Tells Mautic what themes it should support when enabled 57 | 'grapesjsbuilder.integration.builder' => [ 58 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\Integration\Support\BuilderSupport::class, 59 | 'tags' => [ 60 | 'mautic.builder_integration', 61 | ], 62 | ], 63 | ], 64 | 'models' => [ 65 | 'grapesjsbuilder.model' => [ 66 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\Model\GrapesJsBuilderModel::class, 67 | 'arguments' => [ 68 | 'request_stack', 69 | 'mautic.email.model.email', 70 | ], 71 | ], 72 | ], 73 | 'helpers' => [ 74 | 'grapesjsbuilder.helper.filemanager' => [ 75 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\Helper\FileManager::class, 76 | 'arguments' => [ 77 | 'mautic.helper.file_uploader', 78 | 'mautic.helper.core_parameters', 79 | 'mautic.helper.paths', 80 | ], 81 | ], 82 | ], 83 | 'events' => [ 84 | 'grapesjsbuilder.event.assets.subscriber' => [ 85 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\EventSubscriber\AssetsSubscriber::class, 86 | 'arguments' => [ 87 | 'grapesjsbuilder.config', 88 | 'mautic.install.service', 89 | ], 90 | ], 91 | 'grapesjsbuilder.event.email.subscriber' => [ 92 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\EventSubscriber\EmailSubscriber::class, 93 | 'arguments' => [ 94 | 'grapesjsbuilder.config', 95 | 'grapesjsbuilder.model', 96 | ], 97 | ], 98 | 'grapesjsbuilder.event.content.subscriber' => [ 99 | 'class' => \MauticPlugin\GrapesJsBuilderBundle\EventSubscriber\InjectCustomContentSubscriber::class, 100 | 'arguments' => [ 101 | 'grapesjsbuilder.config', 102 | 'grapesjsbuilder.model', 103 | 'grapesjsbuilder.helper.filemanager', 104 | 'mautic.helper.templating', 105 | 'request_stack', 106 | 'router', 107 | ], 108 | ], 109 | ], 110 | ], 111 | 'parameters' => [ 112 | 'image_path_exclude' => ['flags', 'mejs'], // exclude certain folders from showing in the image browser 113 | 'static_url' => '', // optional base url for images 114 | ], 115 | ]; 116 | -------------------------------------------------------------------------------- /Controller/FileManagerController.php: -------------------------------------------------------------------------------- 1 | get('grapesjsbuilder.helper.filemanager'); 20 | 21 | return $this->sendJsonResponse(['data'=> $fileManager->uploadFiles($this->request)]); 22 | } 23 | 24 | /** 25 | * @param string $fileName 26 | * 27 | * @return JsonResponse 28 | */ 29 | public function deleteAction() 30 | { 31 | /** @var FileManager $fileManager */ 32 | $fileManager = $this->get('grapesjsbuilder.helper.filemanager'); 33 | 34 | $fileName = $this->request->get('filename'); 35 | 36 | $fileManager->deleteFile($fileName); 37 | 38 | return $this->sendJsonResponse(['success'=> true]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Controller/GrapesJsController.php: -------------------------------------------------------------------------------- 1 | logger = $this->get('monolog.logger.mautic'); 39 | 40 | /** @var \Mautic\EmailBundle\Model\EmailModel|\Mautic\PageBundle\Model\PageModel $model */ 41 | $model = $this->getModel($objectType); 42 | $aclToCheck = 'email:emails:'; 43 | 44 | if ('page' === $objectType) { 45 | $aclToCheck = 'page:pages:'; 46 | } 47 | 48 | //permission check 49 | if (false !== strpos($objectId, 'new')) { 50 | $isNew = true; 51 | 52 | if (!$this->get('mautic.security')->isGranted($aclToCheck.'create')) { 53 | return $this->accessDenied(); 54 | } 55 | 56 | /** @var \Mautic\EmailBundle\Entity\Email|\Mautic\PageBundle\Entity\Page $entity */ 57 | $entity = $model->getEntity(); 58 | $entity->setSessionId($objectId); 59 | } else { 60 | /** @var \Mautic\EmailBundle\Entity\Email|\Mautic\PageBundle\Entity\Page $entity */ 61 | $entity = $model->getEntity($objectId); 62 | $isNew = false; 63 | 64 | if (null == $entity 65 | || !$this->get('mautic.security')->hasEntityAccess( 66 | $aclToCheck.'viewown', 67 | $aclToCheck.'viewother', 68 | $entity->getCreatedBy() 69 | ) 70 | ) { 71 | return $this->accessDenied(); 72 | } 73 | } 74 | 75 | $slots = []; 76 | $type = 'html'; 77 | $template = InputHelper::clean($this->request->query->get('template')); 78 | if (!$template) { 79 | $this->logger->warn('Grapesjs: no template in query'); 80 | 81 | return $this->json(false); 82 | } 83 | $templateName = ':'.$template.':'.$objectType; 84 | $content = $entity->getContent(); 85 | /** @var ThemeHelper $themeHelper */ 86 | $themeHelper = $this->get('mautic.helper.theme'); 87 | 88 | // Check for MJML template 89 | // @deprecated - use mjml directly in email.html.twig 90 | if ($logicalName = $this->checkForMjmlTemplate($templateName.'.mjml.twig')) { 91 | $type = 'mjml'; 92 | } else { 93 | $logicalName = $themeHelper->checkForTwigTemplate($templateName.'.html.twig'); 94 | $slots = $themeHelper->getTheme($template)->getSlots($objectType); 95 | 96 | //merge any existing changes 97 | $newContent = $this->get('session')->get('mautic.'.$objectType.'builder.'.$objectId.'.content', []); 98 | 99 | if (is_array($newContent)) { 100 | $content = array_merge($content, $newContent); 101 | // Update the content for processSlots 102 | $entity->setContent($content); 103 | } 104 | 105 | if ('page' === $objectType) { 106 | $this->processPageSlots($slots, $entity); 107 | } else { 108 | $this->processEmailSlots($slots, $entity); 109 | } 110 | } 111 | 112 | // Replace short codes to emoji 113 | $content = EmojiHelper::toEmoji($content, 'short'); 114 | 115 | $renderedTemplate = $this->renderView( 116 | $logicalName, 117 | [ 118 | 'isNew' => $isNew, 119 | 'slots' => $slots, 120 | 'content' => $content, 121 | $objectType => $entity, 122 | 'template' => $template, 123 | 'basePath' => $this->request->getBasePath(), 124 | ] 125 | ); 126 | 127 | if (false !== strpos($renderedTemplate, '')) { 128 | $type = 'mjml'; 129 | } 130 | 131 | $renderedTemplateHtml = ('html' === $type) ? $renderedTemplate : ''; 132 | $renderedTemplateMjml = ('mjml' === $type) ? $renderedTemplate : ''; 133 | 134 | return $this->render( 135 | 'GrapesJsBuilderBundle:Builder:template.html.php', 136 | [ 137 | 'templateHtml' => $renderedTemplateHtml, 138 | 'templateMjml' => $renderedTemplateMjml, 139 | ] 140 | ); 141 | } 142 | 143 | /** 144 | * PreProcess email slots for public view. 145 | * 146 | * @param array $slots 147 | * @param Email $entity 148 | */ 149 | private function processEmailSlots($slots, $entity) 150 | { 151 | /** @var \Mautic\CoreBundle\Templating\Helper\SlotsHelper $slotsHelper */ 152 | $slotsHelper = $this->get('templating.helper.slots'); 153 | $content = $entity->getContent(); 154 | 155 | //Set the slots 156 | foreach ($slots as $slot => $slotConfig) { 157 | //support previous format where email slots are not defined with config array 158 | if (is_numeric($slot)) { 159 | $slot = $slotConfig; 160 | $slotConfig = []; 161 | } 162 | 163 | $value = isset($content[$slot]) ? $content[$slot] : ''; 164 | $slotsHelper->set($slot, "
{$value}
"); 165 | } 166 | 167 | //add builder toolbar 168 | $slotsHelper->start('builder'); ?> 169 | 170 | stop(); 172 | } 173 | 174 | /** 175 | * PreProcess page slots for public view. 176 | * 177 | * @param array $slots 178 | * @param Page $entity 179 | */ 180 | private function processPageSlots($slots, $entity) 181 | { 182 | /** @var \Mautic\CoreBundle\Templating\Helper\AssetsHelper $assetsHelper */ 183 | $assetsHelper = $this->get('templating.helper.assets'); 184 | /** @var \Mautic\CoreBundle\Templating\Helper\SlotsHelper $slotsHelper */ 185 | $slotsHelper = $this->get('templating.helper.slots'); 186 | $formFactory = $this->get('form.factory'); 187 | 188 | $slotsHelper->inBuilder(true); 189 | 190 | $content = $entity->getContent(); 191 | 192 | foreach ($slots as $slot => $slotConfig) { 193 | // backward compatibility - if slotConfig array does not exist 194 | if (is_numeric($slot)) { 195 | $slot = $slotConfig; 196 | $slotConfig = []; 197 | } 198 | 199 | // define default config if does not exist 200 | if (!isset($slotConfig['type'])) { 201 | $slotConfig['type'] = 'html'; 202 | } 203 | 204 | if (!isset($slotConfig['placeholder'])) { 205 | $slotConfig['placeholder'] = 'mautic.page.builder.addcontent'; 206 | } 207 | 208 | $value = isset($content[$slot]) ? $content[$slot] : ''; 209 | 210 | if ('slideshow' == $slotConfig['type']) { 211 | if (isset($content[$slot])) { 212 | $options = json_decode($content[$slot], true); 213 | } else { 214 | $options = [ 215 | 'width' => '100%', 216 | 'height' => '250px', 217 | 'background_color' => 'transparent', 218 | 'arrow_navigation' => false, 219 | 'dot_navigation' => true, 220 | 'interval' => 5000, 221 | 'pause' => 'hover', 222 | 'wrap' => true, 223 | 'keyboard' => true, 224 | ]; 225 | } 226 | 227 | // Create sample slides for first time or if all slides were deleted 228 | if (empty($options['slides'])) { 229 | $options['slides'] = [ 230 | [ 231 | 'order' => 0, 232 | 'background-image' => $assetsHelper->getUrl('media/images/mautic_logo_lb200.png'), 233 | 'captionheader' => 'Caption 1', 234 | ], 235 | [ 236 | 'order' => 1, 237 | 'background-image' => $assetsHelper->getUrl('media/images/mautic_logo_db200.png'), 238 | 'captionheader' => 'Caption 2', 239 | ], 240 | ]; 241 | } 242 | 243 | // Order slides 244 | usort( 245 | $options['slides'], 246 | function ($a, $b) { 247 | return strcmp($a['order'], $b['order']); 248 | } 249 | ); 250 | 251 | $options['slot'] = $slot; 252 | $options['public'] = false; 253 | 254 | // create config form 255 | $options['configForm'] = $formFactory->createNamedBuilder( 256 | null, 257 | 'slideshow_config', 258 | [], 259 | ['data' => $options] 260 | )->getForm()->createView(); 261 | 262 | // create slide config forms 263 | foreach ($options['slides'] as $key => &$slide) { 264 | $slide['key'] = $key; 265 | $slide['slot'] = $slot; 266 | $slide['form'] = $formFactory->createNamedBuilder( 267 | null, 268 | 'slideshow_slide_config', 269 | [], 270 | ['data' => $slide] 271 | )->getForm()->createView(); 272 | } 273 | 274 | $renderingEngine = $this->get('templating'); 275 | 276 | if (method_exists($renderingEngine, 'getEngine')) { 277 | $renderingEngine->getEngine('MauticPageBundle:Page:Slots/slideshow.html.php'); 278 | } 279 | $slotsHelper->set($slot, $renderingEngine->render('MauticPageBundle:Page:Slots/slideshow.html.php', $options)); 280 | } else { 281 | $slotsHelper->set($slot, "
{$value}
"); 282 | } 283 | } 284 | 285 | $slotsHelper->start('builder'); ?> 286 | 287 | stop(); 289 | } 290 | 291 | private function checkForMjmlTemplate($template) 292 | { 293 | $templatingHelper = $this->get('mautic.helper.templating'); 294 | 295 | $parser = $templatingHelper->getTemplateNameParser(); 296 | $templating = $templatingHelper->getTemplating(); 297 | $template = $parser->parse($template); 298 | 299 | $twigTemplate = clone $template; 300 | $twigTemplate->set('engine', 'twig'); 301 | 302 | if ($templating->exists($twigTemplate)) { 303 | return $twigTemplate->getLogicalName(); 304 | } 305 | 306 | return null; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /Demo/data/response.template.blank-mjml.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateHtml": "", 3 | "templateMjml": "\n \n \n \n Hello there!\n This is MJML version of blank template for Mautic.\n {unsubscribe_text} | {webview_text}\n \n \n \n" 4 | } 5 | -------------------------------------------------------------------------------- /Demo/data/response.template.blank.json: -------------------------------------------------------------------------------- 1 | { 2 | "templateHtml": "\n\n\n\n \n {subject}\n \n \n \n \n \n \n\n\n
\n
\n \n \n \n \n \n \n
\n
\n \n \n \n \n \n \n
\n \n \n \n \n
\n
\n

Hello World!

\n Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aliquid officia consequatur placeat reprehenderit excepturi, tempore,\n id quos quaerat ab fuga.\n
\n
Lorem ipsum dolor sit amet, consectetur adipisicing elit.\n Inventore, voluptate.\n
\n
Lorem ipsum dolor sit amet, consectetur adipisicing elit.\n Dignissimos alias rerum nemo ducimus modi perspiciatis.\n
\n
\n
\n
\n
\n \n \n \n \n
\n
\n {unsubscribe_text} | {webview_text}\n
\n
\n
\n
\n
\n
\n\n\n", 3 | "templateMjml": "" 4 | } 5 | -------------------------------------------------------------------------------- /Demo/helloWorld/helloWorld.js: -------------------------------------------------------------------------------- 1 | import 'grapesjs/dist/css/grapes.min.css'; 2 | import grapesJS from 'grapesjs'; 3 | import grapesJSMJML from 'grapesjs-mjml'; 4 | 5 | const editor = grapesJS.init({ 6 | fromElement: 1, 7 | container: '#gjs', 8 | avoidInlineStyle: false, 9 | plugins: [grapesJSMJML], 10 | pluginsOpts: { 11 | [grapesJSMJML]: { 12 | // The font imports are included on HTML when fonts are used on the template 13 | fonts: { 14 | Montserrat: 'https://fonts.googleapis.com/css?family=Montserrat', 15 | 'Open Sans': 'https://fonts.googleapis.com/css?family=Open+Sans', 16 | }, 17 | }, 18 | }, 19 | }); 20 | 21 | // add custom fonts options on editor's font list 22 | editor.on('load', () => { 23 | const styleManager = editor.StyleManager; 24 | const fontProperty = styleManager.getProperty('typography', 'font-family'); 25 | 26 | const list = []; 27 | // empty list 28 | fontProperty.set('list', list); 29 | 30 | // custom list 31 | list.push(fontProperty.addOption({ value: 'Montserrat, sans-serif', name: 'Montserrat' })); 32 | list.push(fontProperty.addOption({ value: 'Open Sans, sans-serif', name: 'Open Sans' })); 33 | fontProperty.set('list', list); 34 | 35 | styleManager.render(); 36 | }); 37 | -------------------------------------------------------------------------------- /Demo/helloWorld/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World 4 | 5 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | My Company 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Demo/helloWorld/style.css: -------------------------------------------------------------------------------- 1 | /* Let's highlight canvas boundaries */ 2 | #gjs { 3 | border: 3px solid #444; 4 | } 5 | 6 | /* Reset some default styling */ 7 | .gjs-cv-canvas { 8 | top: 0; 9 | width: 100%; 10 | height: 100%; 11 | } -------------------------------------------------------------------------------- /Demo/mautic/index.html: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Hello Mautic 49 | 50 | 51 |
52 |
53 | 54 |

HTML

55 | 56 |

MJML

57 | 58 |

Assets

59 | 67 |

Builder Url

68 | 74 |

dynamicContentPrototype

75 |
76 | 77 |
78 | Top | 79 | Launch page | 80 | Launch emailform HTML 81 | Launch emailform MJML 82 | 83 | 88 | 89 | -------------------------------------------------------------------------------- /Entity/GrapesJsBuilder.php: -------------------------------------------------------------------------------- 1 | setTable('bundle_grapesjsbuilder') 33 | ->setCustomRepositoryClass(GrapesJsBuilderRepository::class) 34 | ->addNamedField('customMjml', Type::TEXT, 'custom_mjml', true) 35 | ->addId(); 36 | 37 | $builder->createManyToOne( 38 | 'email', 39 | 'Mautic\EmailBundle\Entity\Email' 40 | )->addJoinColumn('email_id', 'id', true, false, 'CASCADE')->build(); 41 | } 42 | 43 | /** 44 | * Get id. 45 | * 46 | * @return int 47 | */ 48 | public function getId() 49 | { 50 | return $this->id; 51 | } 52 | 53 | /** 54 | * @return Email 55 | */ 56 | public function getEmail() 57 | { 58 | return $this->email; 59 | } 60 | 61 | /** 62 | * @return GrapesJsBuilder 63 | */ 64 | public function setEmail(Email $email) 65 | { 66 | $this->email = $email; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function getCustomMjml() 75 | { 76 | return $this->customMjml; 77 | } 78 | 79 | /** 80 | * @param string $customMjml 81 | * 82 | * @return GrapesJsBuilder 83 | */ 84 | public function setCustomMjml($customMjml) 85 | { 86 | $this->customMjml = $customMjml; 87 | 88 | return $this; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Entity/GrapesJsBuilderRepository.php: -------------------------------------------------------------------------------- 1 | config = $config; 28 | $this->installer = $installer; 29 | } 30 | 31 | public static function getSubscribedEvents() 32 | { 33 | return [ 34 | CoreEvents::VIEW_INJECT_CUSTOM_ASSETS => ['injectAssets', 0], 35 | ]; 36 | } 37 | 38 | public function injectAssets(CustomAssetsEvent $assetsEvent) 39 | { 40 | if (!$this->installer->checkIfInstalled()) { 41 | return; 42 | } 43 | if ($this->config->isPublished()) { 44 | $assetsEvent->addScript('plugins/GrapesJsBuilderBundle/Assets/library/js/dist/builder.js'); 45 | $assetsEvent->addStylesheet('plugins/GrapesJsBuilderBundle/Assets/library/js/dist/builder.css'); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /EventSubscriber/EmailSubscriber.php: -------------------------------------------------------------------------------- 1 | config = $config; 31 | $this->grapesJsBuilderModel = $grapesJsBuilderModel; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public static function getSubscribedEvents() 38 | { 39 | return [ 40 | EmailEvents::EMAIL_POST_SAVE => ['onEmailPostSave', 0], 41 | EmailEvents::EMAIL_POST_DELETE => ['onEmailDelete', 0], 42 | ]; 43 | } 44 | 45 | /** 46 | * Add an entry. 47 | */ 48 | public function onEmailPostSave(Events\EmailEvent $event) 49 | { 50 | if (!$this->config->isPublished()) { 51 | return; 52 | } 53 | 54 | $this->grapesJsBuilderModel->addOrEditEntity($event->getEmail()); 55 | } 56 | 57 | /** 58 | * Delete an entry. 59 | */ 60 | public function onEmailDelete(Events\EmailEvent $event) 61 | { 62 | if (!$this->config->isPublished()) { 63 | return; 64 | } 65 | 66 | $email = $event->getEmail(); 67 | $grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $email]); 68 | 69 | if ($grapesJsBuilder) { 70 | $this->grapesJsBuilderModel->getRepository()->deleteEntity($grapesJsBuilder); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /EventSubscriber/InjectCustomContentSubscriber.php: -------------------------------------------------------------------------------- 1 | config = $config; 57 | $this->grapesJsBuilderModel = $grapesJsBuilderModel; 58 | $this->fileManager = $fileManager; 59 | $this->templatingHelper = $templatingHelper; 60 | $this->requestStack = $requestStack; 61 | $this->router = $router; 62 | } 63 | 64 | public static function getSubscribedEvents() 65 | { 66 | return [ 67 | CoreEvents::VIEW_INJECT_CUSTOM_CONTENT => ['injectViewCustomContent', 0], 68 | ]; 69 | } 70 | 71 | public function injectViewCustomContent(CustomContentEvent $customContentEvent) 72 | { 73 | if (!$this->config->isPublished()) { 74 | return; 75 | } 76 | 77 | $passParams = []; 78 | $parameters = $customContentEvent->getVars(); 79 | 80 | if ('email.settings.advanced' === $customContentEvent->getContext()) { 81 | // Inject MJML form within mail page 82 | if (empty($parameters['email']) || !$parameters['email'] instanceof Email) { 83 | return; 84 | } 85 | 86 | $passParams = ['customMjml' => '']; 87 | if ($this->requestStack->getCurrentRequest()->request->has('grapesjsbuilder')) { 88 | $data = $this->requestStack->getCurrentRequest()->get('grapesjsbuilder', ''); 89 | 90 | if (isset($data['customMjml'])) { 91 | $passParams['customMjml'] = $data['customMjml']; 92 | } 93 | } 94 | 95 | $grapesJsBuilder = $this->grapesJsBuilderModel->getRepository()->findOneBy(['email' => $parameters['email']]); 96 | if ('POST' !== $this->requestStack->getCurrentRequest()->getMethod()) { 97 | if (!$grapesJsBuilder instanceof GrapesJsBuilder && $parameters['email']->getClonedId()) { 98 | $grapesJsBuilder = $this->grapesJsBuilderModel->getGrapesJsFromEmailId( 99 | $parameters['email']->getClonedId() 100 | ); 101 | } 102 | 103 | if ($grapesJsBuilder instanceof GrapesJsBuilder) { 104 | $passParams['customMjml'] = $grapesJsBuilder->getCustomMjml(); 105 | } 106 | } 107 | $content = $this->templatingHelper->getTemplating()->render( 108 | 'GrapesJsBuilderBundle:Setting:fields.html.php', 109 | $passParams 110 | ); 111 | 112 | $customContentEvent->addContent($content); 113 | } elseif ('page.header.left' === $customContentEvent->getContext()) { 114 | // Inject fileManager URL and list of images within all pages 115 | $passParams['assets'] = json_encode($this->fileManager->getImages()); 116 | $passParams['dataUpload'] = $this->router->generate('grapesjsbuilder_upload', [], true); 117 | $passParams['dataDelete'] = $this->router->generate('grapesjsbuilder_delete', [], true); 118 | 119 | $content = $this->templatingHelper->getTemplating()->render( 120 | 'GrapesJsBuilderBundle:Setting:vars.html.php', 121 | $passParams 122 | ); 123 | 124 | $customContentEvent->addContent($content); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /GrapesJsBuilderBundle.php: -------------------------------------------------------------------------------- 1 | fileUploader = $fileUploader; 43 | $this->coreParametersHelper = $coreParametersHelper; 44 | $this->pathsHelper = $pathsHelper; 45 | } 46 | 47 | /** 48 | * @param $request 49 | * 50 | * @return array 51 | */ 52 | public function uploadFiles($request) 53 | { 54 | if (isset($request->files->all()['files'])) { 55 | $files = $request->files->all()['files']; 56 | $uploadDir = $this->getUploadDir(); 57 | $uploadedFiles = []; 58 | 59 | foreach ($files as $file) { 60 | try { 61 | $uploadedFiles[] = $this->getFullUrl($this->fileUploader->upload($uploadDir, $file)); 62 | } catch (FileUploadException $e) { 63 | } 64 | } 65 | } 66 | 67 | return $uploadedFiles; 68 | } 69 | 70 | /** 71 | * @param string $fileName 72 | */ 73 | public function deleteFile($fileName) 74 | { 75 | $this->fileUploader->delete($this->getCompleteFilePath($fileName)); 76 | } 77 | 78 | /** 79 | * @param string $fileName 80 | * 81 | * @return string 82 | */ 83 | public function getCompleteFilePath($fileName) 84 | { 85 | $uploadDir = $this->getUploadDir(); 86 | 87 | return $uploadDir.$fileName; 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | private function getUploadDir() 94 | { 95 | return $this->getGrapesJsImagesPath(true); 96 | } 97 | 98 | /** 99 | * @param $fileName 100 | * 101 | * @return string 102 | */ 103 | public function getFullUrl($fileName, $separator = '/') 104 | { 105 | // if a static_url (CDN) is configured use that, otherwiese use the site url 106 | $url = $this->coreParametersHelper->getParameter('static_url') ?? $this->coreParametersHelper->getParameter('site_url'); 107 | 108 | return $url 109 | .$separator 110 | .$this->getGrapesJsImagesPath(false, $separator) 111 | .$fileName; 112 | } 113 | 114 | /** 115 | * @param bool $fullPath 116 | * @param string $separator 117 | * 118 | * @return string 119 | */ 120 | private function getGrapesJsImagesPath($fullPath = false, $separator = '/') 121 | { 122 | return $this->pathsHelper->getSystemPath('images', $fullPath) 123 | .$separator 124 | .self::GRAPESJS_IMAGES_DIRECTORY; 125 | } 126 | 127 | /** 128 | * @return array 129 | */ 130 | public function getImages() 131 | { 132 | $files = []; 133 | $uploadDir = $this->getUploadDir(); 134 | 135 | $fileSystem = new Filesystem(); 136 | 137 | if (!$fileSystem->exists($uploadDir)) { 138 | try { 139 | $fileSystem->mkdir($uploadDir); 140 | } catch (IOException $exception) { 141 | return $files; 142 | } 143 | } 144 | 145 | $finder = new Finder(); 146 | $finder->files()->in($uploadDir); 147 | 148 | foreach ($finder as $file) { 149 | // exclude certain folders from grapesjs file manager 150 | if (in_array($file->getRelativePath(), $this->coreParametersHelper->get('image_path_exclude'))) { 151 | continue; 152 | } 153 | 154 | if ($size = @getimagesize($this->getCompleteFilePath($file->getRelativePathname()))) { 155 | $files[] = [ 156 | 'src' => $this->getFullUrl($file->getRelativePathname()), 157 | 'width' => $size[0], 158 | 'type' => 'image', 159 | 'height' => $size[1], 160 | ]; 161 | } else { 162 | $files[] = $this->getFullUrl($file->getRelativePathname()); 163 | } 164 | } 165 | 166 | return $files; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Integration/Config.php: -------------------------------------------------------------------------------- 1 | integrationsHelper = $integrationsHelper; 21 | } 22 | 23 | public function isPublished(): bool 24 | { 25 | try { 26 | $integration = $this->getIntegrationEntity(); 27 | 28 | return (bool) $integration->getIsPublished() ?: false; 29 | } catch (IntegrationNotFoundException $e) { 30 | return false; 31 | } 32 | } 33 | 34 | /** 35 | * @return mixed[] 36 | */ 37 | public function getFeatureSettings(): array 38 | { 39 | try { 40 | $integration = $this->getIntegrationEntity(); 41 | 42 | return $integration->getFeatureSettings() ?: []; 43 | } catch (IntegrationNotFoundException $e) { 44 | return []; 45 | } 46 | } 47 | 48 | /** 49 | * @throws IntegrationNotFoundException 50 | */ 51 | public function getIntegrationEntity(): Integration 52 | { 53 | $integrationObject = $this->integrationsHelper->getIntegration(GrapesJsBuilderIntegration::NAME); 54 | 55 | return $integrationObject->getIntegrationConfiguration(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Integration/GrapesJsBuilderIntegration.php: -------------------------------------------------------------------------------- 1 | featuresSupported); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Integration/Support/ConfigSupport.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 29 | $this->emailModel = $emailModel; 30 | } 31 | 32 | /** 33 | * @return GrapesJsBuilderRepository 34 | */ 35 | public function getRepository() 36 | { 37 | /** @var GrapesJsBuilderRepository $repository */ 38 | $repository = $this->em->getRepository(GrapesJsBuilder::class); 39 | 40 | $repository->setTranslator($this->translator); 41 | 42 | return $repository; 43 | } 44 | 45 | /** 46 | * Add or edit email settings entity based on request. 47 | */ 48 | public function addOrEditEntity(Email $email) 49 | { 50 | if ($this->emailModel->isUpdatingTranslationChildren()) { 51 | return; 52 | } 53 | 54 | $grapesJsBuilder = $this->getRepository()->findOneBy(['email' => $email]); 55 | 56 | if (!$grapesJsBuilder) { 57 | $grapesJsBuilder = new GrapesJsBuilder(); 58 | $grapesJsBuilder->setEmail($email); 59 | } 60 | 61 | if ($this->requestStack->getCurrentRequest()->request->has('grapesjsbuilder')) { 62 | $data = $this->requestStack->getCurrentRequest()->get('grapesjsbuilder', ''); 63 | 64 | if (isset($data['customMjml'])) { 65 | $grapesJsBuilder->setCustomMjml($data['customMjml']); 66 | } 67 | 68 | $this->getRepository()->saveEntity($grapesJsBuilder); 69 | 70 | $customHtml = $this->requestStack->getCurrentRequest()->get('emailform')['customHtml'] ?? null; 71 | $email->setCustomHtml($customHtml); 72 | $this->emailModel->getRepository()->saveEntity($email); 73 | } 74 | } 75 | 76 | public function getGrapesJsFromEmailId(?int $emailId) 77 | { 78 | if ($email = $this->emailModel->getEntity($emailId)) { 79 | return $this->getRepository()->findOneBy(['email' => $email]); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS Builder with MJML support for Mautic 2 | 3 | ## This plugin is managed centrally in https://github.com/mautic/mautic/blob/head/plugins/GrapesJsBuilderBundle and this is a read-only mirror repository. 4 | 5 | **📣 Please make PRs and issues against Mautic Core, not here!** -------------------------------------------------------------------------------- /Tests/Unit/Model/GrapesJsBuilderModelTest.php: -------------------------------------------------------------------------------- 1 | saveEntityCallCount; 44 | } 45 | }; 46 | 47 | $emailModel = $this->getEmailModel($emailRepository); 48 | 49 | $grapesJsBuilderRepository = new class() extends GrapesJsBuilderRepository { 50 | public $saveEntityCallCount = 0; 51 | 52 | public function __construct() 53 | { 54 | } 55 | 56 | public function findOneBy(array $criteria, ?array $orderBy = null) 57 | { 58 | return null; 59 | } 60 | 61 | public function saveEntity($entity, $flush = true) 62 | { 63 | ++$this->saveEntityCallCount; 64 | } 65 | }; 66 | 67 | $entityManager = new class($grapesJsBuilderRepository) extends EntityManager { 68 | private $grapesJsBuilderRepository; 69 | 70 | public function __construct(GrapesJsBuilderRepository $grapesJsBuilderRepository) 71 | { 72 | $this->grapesJsBuilderRepository = $grapesJsBuilderRepository; 73 | } 74 | 75 | public function getRepository($entityName) 76 | { 77 | Assert::assertSame(GrapesJsBuilder::class, $entityName); 78 | 79 | return $this->grapesJsBuilderRepository; 80 | } 81 | }; 82 | 83 | $email = new Email(); 84 | 85 | $grapeJsBuilderModel = new GrapesJsBuilderModel($requestStack, $emailModel); 86 | $grapeJsBuilderModel->setEntityManager($entityManager); 87 | $grapeJsBuilderModel->setTranslator($this->getTranslator()); 88 | 89 | $grapeJsBuilderModel->addOrEditEntity($email); 90 | 91 | // Not a GrapeJs email, so we are not saving anything. 92 | Assert::assertSame(0, $grapesJsBuilderRepository->saveEntityCallCount); 93 | Assert::assertSame(0, $emailRepository->saveEntityCallCount); 94 | } 95 | 96 | public function testAddOrEditEntityWithoutMatchingEntityAndGrapeRequestQuery(): void 97 | { 98 | $requestStack = new class() extends RequestStack { 99 | public function __construct() 100 | { 101 | } 102 | 103 | public function getCurrentRequest() 104 | { 105 | return new Request( 106 | [], 107 | [ 108 | 'grapesjsbuilder' => [ 109 | 'customMjml' => '
', 110 | ], 111 | 'emailform' => [ 112 | 'customHtml' => '', 113 | ], 114 | ] 115 | ); 116 | } 117 | }; 118 | 119 | $emailRepository = new class() extends EmailRepository { 120 | public $saveEntityCallCount = 0; 121 | 122 | public function __construct() 123 | { 124 | } 125 | 126 | /** 127 | * @param Email $entity 128 | */ 129 | public function saveEntity($entity, $flush = true) 130 | { 131 | ++$this->saveEntityCallCount; 132 | 133 | Assert::assertSame('', $entity->getCustomHtml()); 134 | } 135 | }; 136 | 137 | $emailModel = $this->getEmailModel($emailRepository); 138 | 139 | $grapesJsBuilderRepository = new class() extends GrapesJsBuilderRepository { 140 | public $saveEntityCallCount = 0; 141 | 142 | public function __construct() 143 | { 144 | } 145 | 146 | public function findOneBy(array $criteria, ?array $orderBy = null) 147 | { 148 | return null; 149 | } 150 | 151 | /** 152 | * @param GrapesJsBuilder $entity 153 | */ 154 | public function saveEntity($entity, $flush = true) 155 | { 156 | ++$this->saveEntityCallCount; 157 | 158 | Assert::assertSame('', $entity->getCustomMjml()); 159 | } 160 | }; 161 | 162 | $entityManager = new class($grapesJsBuilderRepository) extends EntityManager { 163 | private $grapesJsBuilderRepository; 164 | 165 | public function __construct(GrapesJsBuilderRepository $grapesJsBuilderRepository) 166 | { 167 | $this->grapesJsBuilderRepository = $grapesJsBuilderRepository; 168 | } 169 | 170 | public function getRepository($entityName) 171 | { 172 | Assert::assertSame(GrapesJsBuilder::class, $entityName); 173 | 174 | return $this->grapesJsBuilderRepository; 175 | } 176 | }; 177 | 178 | $email = new Email(); 179 | 180 | $grapeJsBuilderModel = new GrapesJsBuilderModel($requestStack, $emailModel); 181 | $grapeJsBuilderModel->setEntityManager($entityManager); 182 | $grapeJsBuilderModel->setTranslator($this->getTranslator()); 183 | 184 | $grapeJsBuilderModel->addOrEditEntity($email); 185 | 186 | // Saving the entities now. 187 | Assert::assertSame(1, $grapesJsBuilderRepository->saveEntityCallCount); 188 | Assert::assertSame(1, $emailRepository->saveEntityCallCount); 189 | } 190 | 191 | private function getEmailModel(EmailRepository $emailRepository): EmailModel 192 | { 193 | return new class($emailRepository) extends EmailModel { 194 | private $emailRepository; 195 | 196 | public function __construct(EmailRepository $emailRepository) 197 | { 198 | $this->emailRepository = $emailRepository; 199 | } 200 | 201 | public function getRepository() 202 | { 203 | return $this->emailRepository; 204 | } 205 | }; 206 | } 207 | 208 | private function getTranslator(): Translator 209 | { 210 | return new class() extends Translator { 211 | public function __construct() 212 | { 213 | } 214 | }; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Translations/cs/javascript.ini: -------------------------------------------------------------------------------- 1 | grapesjsbuilder.sourceEditBtnLabel="Upravit" 2 | grapesjsbuilder.sourceCancelBtnLabel="Zrušit" 3 | grapesjsbuilder.sourceEditModalTitle="Upravit kód" 4 | grapesjsbuilder.deleteAssetConfirmText="Opravdu chete smazat tento soubor?" 5 | grapesjsbuilder.categorySectionLabel="Sekce" 6 | grapesjsbuilder.categoryBlockLabel="Bloky" 7 | grapesjsbuilder.dynamicContentBlockLabel="Dynamický obsah" 8 | grapesjsbuilder.dynamicContentBtnLabel="Uložit" 9 | grapesjsbuilder.dynamicContentModalTitle="Upravit dynamický obsah" 10 | grapesjsbuilder.assetManager.noAssets="Nejsou zde žádné assety, přetáhněte sem soubory pro nahrání." 11 | grapesjsbuilder.buttonBlockLabel="Tlačítko" 12 | grapesjsbuilder.components.names.twoColumnThirdSevens="2 Sloupce 3/7" 13 | grapesjsbuilder.components.names.textSectionBlkLabel="Textová část" 14 | grapesjsbuilder.components.names.gridItemsBlkLabel="Položky mřížky" 15 | grapesjsbuilder.components.names.listItemsBlkLabel="Položky seznamu" 16 | -------------------------------------------------------------------------------- /Translations/de/javascript.ini: -------------------------------------------------------------------------------- 1 | grapesjsbuilder.sourceEditBtnLabel="Speichern" 2 | grapesjsbuilder.sourceCancelBtnLabel="Abbrechen" 3 | grapesjsbuilder.sourceEditModalTitle="Code bearbeiten" 4 | grapesjsbuilder.sourceSyntaxError="Bitte korrigieren Sie den folgenden Fehler:" 5 | grapesjsbuilder.deleteAssetConfirmText="Möchten Sie diese Datei wirklich löschen?" 6 | grapesjsbuilder.categorySectionLabel="Abschnitte" 7 | grapesjsbuilder.categoryBlockLabel="Blöcke" 8 | grapesjsbuilder.dynamicContentBlockLabel="Dynamischer Inhalt" 9 | grapesjsbuilder.dynamicContentBtnLabel="Speichern" 10 | grapesjsbuilder.dynamicContentModalTitle="Dynamischen Inhalt bearbeiten" 11 | grapesjsbuilder.assetManager.noAssets="Keine Assets hier, zum Hochladen ziehen" 12 | grapesjsbuilder.buttonBlockLabel="Button" 13 | grapesjsbuilder.components.names.twoColumnThirdSevens="2 Säulen 3/7" 14 | grapesjsbuilder.components.names.textSectionBlkLabel="Text Abschnitt" 15 | grapesjsbuilder.components.names.gridItemsBlkLabel="Raster Elemente" 16 | grapesjsbuilder.components.names.listItemsBlkLabel="Elemente auflisten" 17 | -------------------------------------------------------------------------------- /Translations/en_US/javascript.ini: -------------------------------------------------------------------------------- 1 | grapesjsbuilder.sourceEditBtnLabel="Save" 2 | grapesjsbuilder.sourceCancelBtnLabel="Cancel" 3 | grapesjsbuilder.sourceEditModalTitle="Edit code" 4 | grapesjsbuilder.sourceSyntaxError="Please fix the following error:" 5 | grapesjsbuilder.deleteAssetConfirmText="Are you sure you wish to delete this file?" 6 | grapesjsbuilder.categorySectionLabel="Sections" 7 | grapesjsbuilder.categoryBlockLabel="Blocks" 8 | grapesjsbuilder.dynamicContentBlockLabel="Dynamic Content" 9 | grapesjsbuilder.dynamicContentBtnLabel="Save" 10 | grapesjsbuilder.dynamicContentModalTitle="Edit Dynamic Content" 11 | grapesjsbuilder.buttonBlockLabel="Button" 12 | grapesjsbuilder.components.names.twoColumnThirdSevens="2 Columns 3/7" 13 | grapesjsbuilder.components.names.textSectionBlkLabel="Text Section" 14 | grapesjsbuilder.components.names.gridItemsBlkLabel="Grid Items" 15 | grapesjsbuilder.components.names.listItemsBlkLabel="List Items" 16 | -------------------------------------------------------------------------------- /Translations/fr/javascript.ini: -------------------------------------------------------------------------------- 1 | grapesjsbuilder.sourceEditBtnLabel="Enregistrer" 2 | grapesjsbuilder.sourceCancelBtnLabel="Annuler" 3 | grapesjsbuilder.sourceEditModalTitle="Éditer le code" 4 | grapesjsbuilder.deleteAssetConfirmText="Êtes-vous sûr de vouloir supprimer ce fichier ?" 5 | grapesjsbuilder.categorySectionLabel="Sections" 6 | grapesjsbuilder.categoryBlockLabel="Blocs" 7 | grapesjsbuilder.dynamicContentBlockLabel="Contenu dynamique" 8 | grapesjsbuilder.dynamicContentBtnLabel="Valider" 9 | grapesjsbuilder.dynamicContentModalTitle="Éditer le contenu dynamique" 10 | grapesjsbuilder.assetManager.noAssets="Pas d'images, déposez des fichiers ici pour en ajouter" 11 | grapesjsbuilder.buttonBlockLabel="Bouton" 12 | grapesjsbuilder.components.names.twoColumnThirdSevens="2 Colonnes 3/7" 13 | grapesjsbuilder.components.names.textSectionBlkLabel="Section de texte" 14 | grapesjsbuilder.components.names.gridItemsBlkLabel="Éléments de la grille" 15 | grapesjsbuilder.components.names.listItemsBlkLabel="Éléments de liste" -------------------------------------------------------------------------------- /Translations/pt_BR/javascript.ini: -------------------------------------------------------------------------------- 1 | grapesjsbuilder.sourceEditBtnLabel="Editar" 2 | grapesjsbuilder.sourceCancelBtnLabel="Cancelar" 3 | grapesjsbuilder.sourceEditModalTitle="Editar HTML" 4 | grapesjsbuilder.deleteAssetConfirmText="Tem certeza que deseja remover este arquivo?" 5 | grapesjsbuilder.categorySectionLabel="Seções" 6 | grapesjsbuilder.categoryBlockLabel="Blocos" 7 | grapesjsbuilder.dynamicContentBlockLabel="Conteúdo Dinâmico" 8 | grapesjsbuilder.dynamicContentBtnLabel="Salvar" 9 | grapesjsbuilder.dynamicContentModalTitle="Alterar Conteúdo Dinâmico" 10 | grapesjsbuilder.assetManager.noAssets="Nenhum assets aqui, arraste para fazer Upload" 11 | grapesjsbuilder.buttonBlockLabel="Botón" 12 | grapesjsbuilder.components.names.twoColumnThirdSevens="2 Colunas 3/7" 13 | grapesjsbuilder.components.names.textSectionBlkLabel="Seção de Texto" 14 | grapesjsbuilder.components.names.gridItemsBlkLabel="Itens da grade" 15 | grapesjsbuilder.components.names.listItemsBlkLabel="Itens da lista" -------------------------------------------------------------------------------- /Views/Builder/template.html.php: -------------------------------------------------------------------------------- 1 | $templateHtml, 5 | 'templateMjml' => $templateMjml, 6 | ]); 7 | -------------------------------------------------------------------------------- /Views/Setting/fields.html.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 |
8 | -------------------------------------------------------------------------------- /Views/Setting/vars.html.php: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mautic/grapes-js-builder-bundle", 3 | "description": "GrapesJS Builder with MJML support for Mautic", 4 | "type": "mautic-plugin", 5 | "keywords": [ 6 | "mautic", 7 | "plugin", 8 | "integration" 9 | ], 10 | "extra": { 11 | "install-directory-name": "GrapesJsBuilderBundle" 12 | }, 13 | "require-dev": { 14 | "phpstan/phpstan": "^0.11.12", 15 | "symplify/easy-coding-standard": "^6.0" 16 | }, 17 | "scripts": { 18 | "test": [ 19 | "@phpunit", 20 | "@fixcs", 21 | "@phpstan" 22 | ], 23 | "quicktest": [ 24 | "@unit" 25 | ], 26 | "phpunit": "../../bin/phpunit -d memory_limit=2048M --bootstrap ../../vendor/autoload.php --configuration phpunit.xml --fail-on-warning --testsuite=all", 27 | "unit": "../../bin/phpunit -d memory_limit=2048M --bootstrap ../../vendor/autoload.php --configuration phpunit.xml --fail-on-warning --testsuite=unit", 28 | "coverage": "../../bin/phpunit -d memory_limit=2048M --bootstrap ../../vendor/autoload.php --configuration phpunit.xml --fail-on-warning --testsuite=all --coverage-text --coverage-html=Tests/Coverage", 29 | "phpstan": "vendor/bin/phpstan analyse --autoload-file=../../vendor/autoload.php --level=max Config Connection Entity Form Integration Migrations Sync Tests", 30 | "csfixer": "vendor/bin/ecs check .", 31 | "fixcs": "vendor/bin/ecs check . --fix" 32 | }, 33 | "minimum-stability": "dev", 34 | "require": { 35 | "php": ">=7.4.0 <8.1", 36 | "mautic/core-lib": "^4.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /easy-coding-standard.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: 'vendor/symplify/easy-coding-standard/config/set/clean-code.yaml' } 3 | - { resource: 'vendor/symplify/easy-coding-standard/config/set/php71.yaml' } 4 | - { resource: 'vendor/symplify/easy-coding-standard/config/set/symfony.yaml' } 5 | 6 | services: 7 | PhpCsFixer\Fixer\Operator\BinaryOperatorSpacesFixer: 8 | align_equals: true 9 | align_double_arrow: true 10 | 11 | parameters: 12 | exclude_checkers: 13 | - 'PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer' 14 | - 'Symplify\CodingStandard\Fixer\Commenting\RemoveUselessDocBlockFixer' 15 | - 'PhpCsFixer\Fixer\Import\OrderedImportsFixer' 16 | exclude_files: 17 | - 'vendor/*' 18 | skip: 19 | PhpCsFixer\Fixer\ArraNotation\TrailingCommaInMultilineArrayFixer: 20 | - 'Config/config.php' # Forces us to do not use full path class names 21 | SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingParameterTypeHint: 22 | - 'Tests/*' 23 | SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.MissingPropertyTypeHint: 24 | - 'Tests/*' 25 | SlevomatCodingStandard\Sniffs\Classes\UnusedPrivateElementsSniff.UnusedMethod: 26 | - 'Connection/Client.php' 27 | SlevomatCodingStandard\Sniffs\Classes\UnusedPrivateElementsSniff.WriteOnlyProperty: 28 | - 'Connection/Client.php' 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjsbuilderbundle", 3 | "version": "1.0.0", 4 | "description": "⚠️ This Plugin is still Beta! It works great already and we're developing it actively! Please use it and report everything inside the \"Issues\" here in Github. ⚠️", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "parcel build Assets/library/js/builder.js --out-dir Assets/library/js/dist --public-url ./", 8 | "build-dev": "NODE_ENV=development parcel build Assets/library/js/builder.js --out-dir Assets/library/js/dist --public-url ./ --no-minify ", 9 | "dev": "NODE_ENV=development parcel watch Assets/library/js/* --out-dir Assets/library/js/dist --public-url /plugins/GrapesJsBuilderBundle/Assets/library/js/dist --hmr-hostname localhost", 10 | "lint": "eslint Assets/library/js/", 11 | "prettier": "node_modules/.bin/prettier -w Assets/library/js/", 12 | "prettier-check": "node_modules/.bin/prettier -c Assets/library/js/", 13 | "start-helloWorld": "parcel Demo/helloWorld/index.html", 14 | "start-mautic-full": "cp -r Demo/data dist && parcel Demo/mautic/full.html", 15 | "start-mautic": "cp -r Demo/data dist && parcel Demo/mautic/index.html", 16 | "update-mautic-preset": "rm -r node_modules/grapesjs-preset-mautic && npm install mautic/grapesjs-preset-mautic#main" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/mautic/plugin-grapesjs-builder.git" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/mautic/plugin-grapesjs-builder/issues" 26 | }, 27 | "homepage": "https://github.com/mautic/plugin-grapesjs-builder#readme", 28 | "dependencies": { 29 | "grapesjs": "^0.17.29", 30 | "grapesjs-mjml": "^0.5.8", 31 | "grapesjs-parser-postcss": "^0.1.1", 32 | "grapesjs-plugin-ckeditor": "^0.0.10", 33 | "grapesjs-preset-mautic": "github:mautic/grapesjs-preset-mautic#main", 34 | "grapesjs-preset-newsletter": "^0.2.21", 35 | "grapesjs-preset-webpage": "^0.1.11" 36 | }, 37 | "devDependencies": { 38 | "@babel/cli": "^7.16.8", 39 | "@babel/core": "^7.16.7", 40 | "@babel/plugin-proposal-class-properties": "^7.16.7", 41 | "@babel/plugin-transform-runtime": "^7.16.8", 42 | "babel-eslint": "^10.1.0", 43 | "eslint": "^8.7.0", 44 | "eslint-config-airbnb": "^19.0.4", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-plugin-import": "^2.25.4", 47 | "eslint-plugin-jsx-a11y": "^6.5.1", 48 | "eslint-plugin-prettier": "^4.0.0", 49 | "eslint-plugin-react": "^7.28.0", 50 | "eslint-plugin-react-hooks": "^4.3.0", 51 | "parcel-bundler": "^1.12.4", 52 | "prettier": "^2.5.1", 53 | "sass": "^1.48.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#.*|PHPUnit_Framework_MockObject_MockObject given$#' 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 18 | Tests/Unit 19 | 20 | 21 | Tests/Functional 22 | 23 | 24 | Tests/Unit 25 | Tests/Functional 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | * 36 | 37 | Assets 38 | Config 39 | Tests 40 | Translations 41 | Views 42 | vendor 43 | 44 | 45 | 46 | 47 | 48 | --------------------------------------------------------------------------------