├── .browserslistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── release-plugin-zip.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .stylelintrc ├── LICENSE ├── README.md ├── assets ├── css │ ├── admin │ │ └── admin-style.css │ ├── frontend │ │ ├── base │ │ │ └── index.css │ │ ├── components │ │ │ └── index.css │ │ ├── editor-style.css │ │ ├── global │ │ │ ├── colors.css │ │ │ ├── index.css │ │ │ └── media-queries.css │ │ ├── layout │ │ │ └── index.css │ │ ├── style.css │ │ └── templates │ │ │ └── index.css │ └── shared │ │ ├── chat.scss │ │ ├── form.scss │ │ └── shared-style.css ├── fonts │ └── font-name │ │ └── .gitkeep ├── images │ └── .gitkeep ├── js │ ├── admin │ │ └── admin.js │ ├── frontend │ │ ├── components │ │ │ └── .gitkeep │ │ └── frontend.js │ └── shared │ │ ├── chat.js │ │ ├── form.js │ │ └── shared.js └── svg │ └── .gitkeep ├── babel.config.js ├── composer.json ├── composer.lock ├── config ├── webpack.config.common.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpack.settings.js ├── dist ├── css │ ├── chat-style.css │ ├── form-style.css │ └── style.css └── js │ ├── chat.js │ └── form.js ├── examples └── zammad-test.php ├── includes ├── classes │ ├── .gitkeep │ ├── Zammad.php │ └── Zammad │ │ ├── Group.php │ │ ├── Organization.php │ │ ├── Ticket.php │ │ └── User.php └── functions │ ├── chat.php │ ├── core.php │ ├── form.php │ └── hf-plugin-integration.php ├── languages └── zammad-wp.pot ├── package-lock.json ├── package.json ├── phpcs.xml ├── phpunit.xml ├── plugin.php ├── postcss.config.js ├── readme.txt ├── script.js ├── style.css ├── tests ├── bootstrap.php ├── js │ └── .gitkeep └── phpunit │ └── test-tools │ ├── Core_Tests.php │ └── TestCase.php └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | > 1% 4 | last 2 versions 5 | IE 11 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = tab 9 | 10 | [{*.json,*.yml,.babelrc,.bowerrc,.browserslistrc,.postcssrc}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.txt,wp-config-sample.php] 15 | end_of_line = crlf 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | assets/js/vendor 2 | assets/js/admin/vendor 3 | assets/js/frontend/vendor 4 | assets/js/shared/vendor 5 | gulp-tasks/ 6 | webpack.config.babel.js 7 | gulpfile.babel.js 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@10up/eslint-config/wordpress", 3 | "rules": {}, 4 | "globals": { 5 | "module": true, 6 | "process": true 7 | }, 8 | "env": { 9 | "browser": true, 10 | "jquery": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/release-plugin-zip.yml: -------------------------------------------------------------------------------- 1 | name: Release installable Package 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | jobs: 7 | tag: 8 | name: Release new tagged version 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checkout Master branch 12 | - uses: actions/checkout@v2 13 | 14 | # Install PHP and Composer 15 | - uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: "8.0" 18 | 19 | # Install Composer Deps 20 | - uses: "ramsey/composer-install@v2" 21 | with: 22 | composer-options: "--no-dev --prefer-dist --no-interaction --quiet --optimize-autoloader" 23 | 24 | # Build Release Zip for GitHub 25 | - name: ZIP Release 26 | uses: thedoctor0/zip-release@master 27 | with: 28 | path: '.' 29 | filename: 'zammad-wp-release.zip' 30 | exclusions: '/tests/* /assets/* /config/* /*.config.js /*.json /*.lock .* *.git* *.xml*' 31 | 32 | # Create GitHub Release 33 | - name: Create Release 34 | id: create_release 35 | uses: actions/create-release@v1 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | tag_name: ${{ github.ref }} 40 | release_name: ${{ github.ref }} 41 | body: Find Changelog here https://github.com/ouun/zammad-wp/blob/master/readme.txt 42 | draft: false 43 | prerelease: false 44 | 45 | # One ZIP upload directly to Release 46 | - name: Upload Release Asset 47 | id: upload-release-asset 48 | uses: actions/upload-release-asset@v1 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 53 | asset_path: ./zammad-wp-release.zip 54 | asset_name: zammad-wp.zip 55 | asset_content_type: application/zip 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | release 4 | vendor 5 | .idea 6 | *.log 7 | 8 | # Editors 9 | *.esproj 10 | *.tmproj 11 | *.tmproject 12 | tmtags 13 | .*.sw[a-z] 14 | *.un~ 15 | Session.vim 16 | *.swp 17 | 18 | # Mac OSX 19 | .DS_Store 20 | ._* 21 | .Spotlight-V100 22 | .Trashes 23 | 24 | # Windows 25 | Thumbs.db 26 | Desktop.ini 27 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@10up/stylelint-config" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zammad for WordPress 2 | 3 | This plugin helps you embed Zammad Chats & Forms into your WordPress site and gives you Access to the Zammad API if required. 4 | It is based on WordPress best practise, keeping your workplace clean by using functions, hooks and filters instead of cluttered dashboard pages. 5 | 6 | ## Documentation 7 | As the documentation grows, please look at the [Zammad WP Wiki](https://github.com/ouun/zammad-wp/wiki/). 8 | 9 | ## Compatibility 10 | Currently, Zammad WP is compatible with the following Form plugins to replace the Zammad standard form with custom & complex ones as documented in the [Wiki](https://github.com/ouun/zammad-wp/wiki/). 11 | 12 | - [HTML Forms by Ibericode](https://github.com/ibericode/html-forms) 13 | 14 | ## Build the package 15 | 16 | ### Webpack config 17 | 18 | Webpack config files can be found in `config` folder: 19 | 20 | - `webpack.config.dev.js` 21 | - `webpack.config.common.js` 22 | - `webpack.config.prod.js` 23 | - `webpack.settings.js` 24 | 25 | In most cases `webpack.settings.js` is the main file which would change from project to project. For example adding or removing entry points for JS and CSS. 26 | 27 | ### NPM Commands 28 | 29 | - `npm run test` (runs phpunit) 30 | - `npm run start` (install dependencies) 31 | - `npm run watch` (watch) 32 | - `npm run build` (build all files) 33 | - `npm run build-release` (build all files for release) 34 | - `npm run dev` (build all files for development) 35 | - `npm run lint-release` (install dependencies and run linting) 36 | - `npm run lint-css` (lint CSS) 37 | - `npm run lint-js` (lint JS) 38 | - `npm run lint-php` (lint PHP) 39 | - `npm run lint` (run all lints) 40 | - `npm run format-js` (format JS using eslint) 41 | - `npm run format` (alias for `npm run format-js`) 42 | - `npm run test-a11y` (run accessibility tests) 43 | 44 | ### Composer Commands 45 | 46 | `composer lint` (lint PHP files) 47 | 48 | `composer lint-fix` (lint PHP files and automatically correct coding standard violations) 49 | 50 | ## Contributing 51 | 52 | We welcome pull requests and spirited, but respectful, debates. Please contribute via [pull requests on GitHub](https://github.com/ouun/zammad-wp/compare). 53 | 54 | 1. Fork it! 55 | 2. Create your feature branch: `git checkout -b feature/my-new-feature` 56 | 3. Commit your changes: `git commit -am 'Added some great feature!'` 57 | 4. Push to the branch: `git push origin feature/my-new-feature` 58 | 5. Submit a pull request 59 | -------------------------------------------------------------------------------- /assets/css/admin/admin-style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ZammadWp - Admin Styles 3 | */ 4 | -------------------------------------------------------------------------------- /assets/css/frontend/base/index.css: -------------------------------------------------------------------------------- 1 | /* Base */ 2 | -------------------------------------------------------------------------------- /assets/css/frontend/components/index.css: -------------------------------------------------------------------------------- 1 | /* Components */ 2 | -------------------------------------------------------------------------------- /assets/css/frontend/editor-style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ZammadWp - Editor Styles 3 | */ 4 | -------------------------------------------------------------------------------- /assets/css/frontend/global/colors.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Colors 3 | */ 4 | :root { 5 | --c-black: #000; 6 | --c-white: #fff; 7 | } 8 | -------------------------------------------------------------------------------- /assets/css/frontend/global/index.css: -------------------------------------------------------------------------------- 1 | @import url("colors.css"); 2 | @import url("media-queries.css"); 3 | -------------------------------------------------------------------------------- /assets/css/frontend/global/media-queries.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Media Queries 3 | */ 4 | @custom-media --bp-tiny ( min-width: 25em ); /* 400px */ 5 | @custom-media --bp-small ( min-width: 30em ); /* 480px */ 6 | @custom-media --bp-medium ( min-width: 48em ); /* 768px */ 7 | @custom-media --bp-large ( min-width: 64em ); /* 1024px */ 8 | @custom-media --bp-xlarge ( min-width: 80em ); /* 1280px */ 9 | @custom-media --bp-xxlarge ( min-width: 90em ); /* 1440px */ 10 | 11 | /* WP Core Breakpoints (used for the admin bar for example) */ 12 | @custom-media --wp-small ( min-width: 600px ); 13 | @custom-media --wp-medium-max (max-width: 782px); 14 | -------------------------------------------------------------------------------- /assets/css/frontend/layout/index.css: -------------------------------------------------------------------------------- 1 | /* Layout */ 2 | -------------------------------------------------------------------------------- /assets/css/frontend/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ZammadWp 3 | */ 4 | 5 | /* Global - global pieces like media queries, mixins and placholders */ 6 | @import url("global/index"); 7 | 8 | /* Base - base styles such as fonts, typography, and wordpress overrides */ 9 | 10 | /* @import url("base/index.css"); */ 11 | 12 | /* Layout - styles specific to layout */ 13 | 14 | /* @import url("layout/index"); */ 15 | 16 | /* Templates */ 17 | 18 | /* @import url("templates/index"); */ 19 | 20 | /* Components */ 21 | 22 | /* @import url("components/index"); */ 23 | -------------------------------------------------------------------------------- /assets/css/frontend/templates/index.css: -------------------------------------------------------------------------------- 1 | /* Templates */ 2 | -------------------------------------------------------------------------------- /assets/css/shared/chat.scss: -------------------------------------------------------------------------------- 1 | $white: #fff; 2 | $black: #000; 3 | 4 | .zammad-chat { 5 | border-radius: 5px 5px 0 0; 6 | bottom: 0; 7 | box-shadow: 0 3px 10px rgba(0, 0, 0, 0.3); 8 | color: $black; 9 | display: none; 10 | flex-direction: column; 11 | font-size: 12px; 12 | height: 3.5em; 13 | position: fixed; 14 | right: 30px; 15 | width: 33em; 16 | will-change: bottom; 17 | z-index: 999; 18 | 19 | @media only screen and (max-width: 768px) { 20 | border-radius: 0 !important; 21 | font-size: 16px; 22 | right: 0 !important; 23 | width: 100%; 24 | } 25 | 26 | &--animate { 27 | transition: transform 500ms; 28 | } 29 | } 30 | 31 | .zammad-chat.zammad-chat-is-loaded { 32 | display: flex; 33 | opacity: 0; 34 | } 35 | 36 | .zammad-chat.zammad-chat-is-shown { 37 | opacity: 1; 38 | 39 | &:not(.zammad-chat-is-open) { 40 | .zammad-chat-modal-text { 41 | display: none; 42 | } 43 | } 44 | } 45 | 46 | .zammad-chat.zammad-chat-is-open { 47 | height: 30em; 48 | 49 | @media only screen and (max-width: 768px) { 50 | height: 100%; 51 | } 52 | } 53 | 54 | .zammad-chat-icon { 55 | fill: currentColor; 56 | height: 2em; 57 | margin-right: 5px; 58 | margin-top: 4px; 59 | vertical-align: top; 60 | width: 2em; 61 | } 62 | 63 | .zammad-chat-header { 64 | background: hsl(203, 67%, 53%); 65 | border-radius: 5px 5px 0 0; 66 | box-shadow: 67 | 0 -1px rgba(0, 0, 0, 0.1), 68 | 0 1px rgba(255, 255, 255, 0.3) inset, 69 | 0 -1px rgba(0, 0, 0, 0.1) inset, 70 | 0 1px 1px rgba(0, 0, 0, 0.13); 71 | color: $white; 72 | cursor: pointer; 73 | height: 3.5em; 74 | line-height: 2.5em; 75 | overflow: hidden; 76 | padding: 0.5em 2.5em 0.5em 1em; 77 | position: relative; 78 | 79 | @media only screen and (max-width: 768px) { 80 | border-radius: 0 !important; 81 | } 82 | } 83 | 84 | .zammad-chat.zammad-chat-is-open .zammad-chat-header { 85 | cursor: default; 86 | } 87 | 88 | .zammad-chat-welcome-text { 89 | font-size: 1.2em; 90 | } 91 | 92 | .zammad-chat-header-icon { 93 | cursor: pointer; 94 | height: 100%; 95 | line-height: 3.4em; 96 | position: absolute; 97 | right: 0; 98 | text-align: center; 99 | top: 0; 100 | width: 3.4em; 101 | 102 | &::before { // force the icon to align to center 103 | content: ""; 104 | display: inline-block; 105 | } 106 | } 107 | 108 | .zammad-chat-header-icon-open, 109 | .zammad-chat-header-icon-close { 110 | fill: currentColor; 111 | height: auto; 112 | vertical-align: middle; 113 | width: 1.6em; 114 | } 115 | 116 | .zammad-chat-header-icon-close { 117 | width: 1.3em; 118 | } 119 | 120 | .zammad-chat-header-icon-close, 121 | .zammad-chat.zammad-chat-is-open .zammad-chat-header-icon-open { 122 | display: none; 123 | } 124 | 125 | .zammad-chat.zammad-chat-is-open .zammad-chat-header-icon-close { 126 | display: inline; 127 | } 128 | 129 | .zammad-chat-agent { 130 | float: left; 131 | } 132 | 133 | .zammad-chat-header-controls { 134 | float: right; 135 | } 136 | 137 | .zammad-chat-agent-avatar { 138 | border-radius: 100%; 139 | float: left; 140 | margin-right: 0.6em; 141 | width: 2.5em; 142 | } 143 | 144 | .zammad-chat-agent-name { 145 | font-weight: bold; 146 | } 147 | 148 | .zammad-chat-agent-status { 149 | background: rgba(0, 0, 0, 0.1); 150 | border-radius: 1em; 151 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; 152 | display: inline-block; 153 | line-height: 2em; 154 | margin: 0.25em 1em; 155 | padding: 0 0.7em; 156 | } 157 | 158 | .zammad-chat-agent-status::before { 159 | background: hsl(19, 90%, 51%); 160 | border-radius: 100%; 161 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; 162 | content: ""; 163 | display: inline-block; 164 | height: 0.9em; 165 | margin-right: 0.3em; 166 | position: relative; 167 | vertical-align: middle; 168 | width: 0.9em; 169 | } 170 | 171 | .zammad-chat-agent-status[data-status="online"]::before { 172 | background: hsl(145, 51%, 55%); 173 | } 174 | 175 | .zammad-chat-agent-status[data-status="connecting"]::before { 176 | animation: linear connect-fade 600ms infinite alternate; 177 | background: hsl(41, 100%, 49%); 178 | } 179 | 180 | @keyframes connect-fade { 181 | 182 | from { 183 | opacity: 0.5; 184 | transform: scale(0.6); 185 | } 186 | 187 | to { 188 | opacity: 1; 189 | transform: scale(1); 190 | } 191 | } 192 | 193 | .zammad-chat-is-open .zammad-chat-modal { 194 | flex-direction: column; 195 | } 196 | 197 | .zammad-chat-modal { 198 | align-items: center; 199 | background: $white; 200 | bottom: 0; 201 | display: flex; 202 | justify-content: center; 203 | left: 0; 204 | margin-top: 1px; 205 | padding: 20px; 206 | position: absolute; 207 | right: 0; 208 | text-align: center; 209 | top: 3.5em; 210 | z-index: 1; 211 | 212 | &:empty { 213 | display: none; 214 | } 215 | 216 | // Fallback form styles 217 | &.zammad-fallback-form { 218 | overflow: auto; 219 | 220 | #fallback-form { 221 | height: 100%; 222 | width: 100%; 223 | 224 | .zammad-form { 225 | padding: 15px 0 30px 0; 226 | } 227 | } 228 | } 229 | } 230 | 231 | .zammad-chat-modal-text { 232 | font-size: 1.3em; 233 | line-height: 1.45; 234 | 235 | .zammad-chat-loading-animation { 236 | font-size: 0.7em; 237 | } 238 | 239 | .zammad-chat-button { 240 | font-size: 0.8em; 241 | margin-top: 1em; 242 | } 243 | } 244 | 245 | .zammad-chat-modal .zammad-chat-loading-animation { 246 | margin-bottom: 8px; 247 | vertical-align: middle; 248 | } 249 | 250 | .zammad-scroll-hint { 251 | align-items: center; 252 | background: hsl(210, 8%, 98%); 253 | border-bottom: 1px solid hsl(0, 0%, 91%); 254 | color: hsl(0, 0%, 60%); 255 | cursor: pointer; 256 | display: flex; 257 | padding: 7px 10px 6px; 258 | 259 | &.is-hidden { 260 | display: none; 261 | } 262 | } 263 | 264 | .zammad-scroll-hint-icon { 265 | fill: hsl(210, 5%, 78%); 266 | margin-right: 8px; 267 | } 268 | 269 | .zammad-chat-body { 270 | background: $white; 271 | display: none; 272 | flex: 1; 273 | overflow: auto; 274 | -webkit-overflow-scrolling: touch; 275 | overscroll-behavior: contain; 276 | padding: 0.5em 1em; 277 | 278 | @media only screen and (max-width: 768px) { 279 | flex: 1; 280 | height: auto; 281 | } 282 | } 283 | 284 | .zammad-chat-is-open .zammad-chat-body { 285 | display: block; 286 | } 287 | 288 | .zammad-chat-timestamp { 289 | color: hsl(0, 0%, 60%); 290 | font-size: 0.9em; 291 | margin: 1em 0; 292 | text-align: center; 293 | } 294 | 295 | .zammad-chat-status { 296 | margin: 1em 0; 297 | text-align: center; 298 | } 299 | 300 | .zammad-chat-message { 301 | margin: 0.5em 0; 302 | } 303 | 304 | 305 | .zammad-chat-message-body { 306 | border-radius: 1em; 307 | word-wrap: break-word; 308 | $white-space: pre-line; 309 | } 310 | 311 | .zammad-chat-status-inner, 312 | .zammad-chat-message-body { 313 | background: hsl(0, 0%, 93%); 314 | box-shadow: 315 | 0 2px rgba(255, 255, 255, 0.15) inset, 316 | 0 0 0 1px rgba(0, 0, 0, 0.08) inset, 317 | 0 1px rgba(0, 0, 0, 0.02); 318 | display: inline-block; 319 | line-height: 1.4; 320 | max-width: 70%; 321 | padding: 0.5em 1em; 322 | } 323 | 324 | .zammad-chat-status-inner { 325 | background: #eee; 326 | border-radius: 0.5em; 327 | } 328 | 329 | .zammad-chat-message--customer { 330 | text-align: right; 331 | } 332 | 333 | .zammad-chat-message--customer + .zammad-chat-message--agent, 334 | .zammad-chat-message--agent + .zammad-chat-message--customer { 335 | margin-top: 1em; 336 | } 337 | 338 | .zammad-chat-message--customer .zammad-chat-message-body { 339 | background: hsl(203, 67%, 53%); 340 | color: $white; 341 | } 342 | 343 | .zammad-chat-message--unread { 344 | font-weight: bold; 345 | } 346 | 347 | .zammad-chat-message--typing .zammad-chat-message-body { 348 | $white-space: normal; 349 | } 350 | 351 | .zammad-chat-loading-animation { 352 | display: inline-block; 353 | } 354 | 355 | .zammad-chat-loading-circle { 356 | animation: ease-in-out load-fade 600ms infinite alternate; 357 | background: hsl(0, 0%, 85%); 358 | border-radius: 100%; 359 | display: inline-block; 360 | height: 0.55em; 361 | width: 0.55em; 362 | } 363 | 364 | .zammad-chat-loading-circle + .zammad-chat-loading-circle { 365 | animation-delay: 0.13s; 366 | } 367 | 368 | .zammad-chat-loading-circle + .zammad-chat-loading-circle + .zammad-chat-loading-circle { 369 | animation-delay: 0.26s; 370 | } 371 | 372 | @keyframes load-fade { 373 | 374 | from { 375 | opacity: 0.5; 376 | transform: scale(0.6); 377 | } 378 | 379 | 67% { 380 | opacity: 1; 381 | transform: scale(1); 382 | } 383 | } 384 | 385 | .zammad-chat-controls { 386 | align-items: flex-end; 387 | background: $white; 388 | border-top: 1px solid hsl(0, 0%, 93%); 389 | box-shadow: 390 | 0 1px rgba(0, 0, 0, 0.01), 391 | 0 -1px rgba(0, 0, 0, 0.02); 392 | display: none; 393 | line-height: 1.4em; 394 | margin: 0; 395 | overflow: hidden; 396 | padding: 0; 397 | position: relative; 398 | } 399 | 400 | .zammad-chat-is-open .zammad-chat-controls { 401 | display: flex; 402 | } 403 | 404 | .zammad-chat-input { 405 | appearance: none; 406 | background: none; 407 | border: none; 408 | box-shadow: none; 409 | box-sizing: content-box; 410 | flex: 1; 411 | float: left; 412 | font-family: inherit; 413 | font-size: inherit; 414 | line-height: 1.4em; 415 | margin: 0; 416 | max-height: 6em; 417 | min-height: 1.4em; 418 | outline: none; 419 | overflow: auto; 420 | padding: 1em 2em; 421 | } 422 | 423 | .zammad-chat-input::-webkit-input-placeholder { 424 | color: hsl(0, 0%, 85%); 425 | } 426 | 427 | .zammad-chat-button { 428 | appearance: none; 429 | background: hsl(203, 67%, 53%); 430 | border: none; 431 | border-radius: 1.5em; 432 | box-shadow: 433 | 0 2px rgba(255, 255, 255, 0.25) inset, 434 | 0 0 0 1px rgba(0, 0, 0, 0.1) inset, 435 | 0 1px rgba(0, 0, 0, 0.1); 436 | color: $white; 437 | cursor: pointer; 438 | display: inline-block; 439 | font-family: inherit; 440 | font-size: inherit; 441 | line-height: initial; 442 | margin: 0.63em 1em; 443 | outline: none; 444 | padding: 0.5em 1.2em; 445 | } 446 | 447 | .zammad-chat-send { 448 | float: right; 449 | } 450 | 451 | .zammad-chat-button:disabled, 452 | .zammad-chat-input:disabled { 453 | opacity: 0.3; 454 | } 455 | 456 | .zammad-chat-is-hidden { 457 | display: none; 458 | } 459 | 460 | /* 461 | # Flat Design 462 | */ 463 | 464 | .zammad-chat--flat .zammad-chat-header, 465 | .zammad-chat--flat .zammad-chat-body { 466 | border: none; 467 | } 468 | 469 | .zammad-chat--flat .zammad-chat-header { 470 | box-shadow: none; 471 | } 472 | 473 | .zammad-chat--flat .zammad-chat-message-body { 474 | box-shadow: none; 475 | } 476 | 477 | .zammad-chat--flat .zammad-chat-agent-status, 478 | .zammad-chat--flat .zammad-chat-button, 479 | .zammad-chat--flat .zammad-chat-status { 480 | box-shadow: none; 481 | } 482 | -------------------------------------------------------------------------------- /assets/css/shared/form.scss: -------------------------------------------------------------------------------- 1 | .zammad-form { 2 | text-align: left; 3 | width: 100%; 4 | } 5 | 6 | .zammad-form h1, 7 | .zammad-form h2 { 8 | margin-top: 0; 9 | } 10 | 11 | .zammad-form .form-group { 12 | margin-bottom: 15px; 13 | } 14 | 15 | .zammad-form .form-control { 16 | display: block; 17 | width: 100%; 18 | } 19 | 20 | .zammad-form .has-error .form-control { 21 | border-color: #a94442; 22 | } 23 | 24 | .zammad-form .has-error label { 25 | color: #a94442; 26 | } 27 | 28 | .zammad-form .js-thankyou { 29 | text-align: center; 30 | } 31 | 32 | .zammad-form-modal { 33 | height: 100%; 34 | left: 0; 35 | position: fixed; 36 | text-align: center; 37 | top: 0; 38 | width: 100%; 39 | z-index: 999; 40 | } 41 | 42 | .zammad-form-modal::before { 43 | content: ""; 44 | display: inline-block; 45 | height: 100%; 46 | margin-right: -0.25em; 47 | vertical-align: middle; 48 | } 49 | 50 | .zammad-form-modal-backdrop { 51 | background-color: gray; 52 | height: 100%; 53 | left: 0; 54 | opacity: 0.7; 55 | position: absolute; 56 | top: 0; 57 | width: 100%; 58 | } 59 | 60 | .zammad-form-modal-body { 61 | background: white; 62 | box-shadow: 0 0 50px rgba(0, 0, 0, 0.3); 63 | box-sizing: border-box; 64 | color: #949494; 65 | display: inline-block; 66 | margin: 0 auto; 67 | max-width: 26em; 68 | padding: 24px 24px 22px; 69 | position: relative; 70 | text-align: left; 71 | vertical-align: middle; 72 | width: 90%; 73 | } 74 | -------------------------------------------------------------------------------- /assets/css/shared/shared-style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * ZammadWp: Shared Styles 3 | */ 4 | -------------------------------------------------------------------------------- /assets/fonts/font-name/.gitkeep: -------------------------------------------------------------------------------- 1 | # Basically just want to ignore the directory contents 2 | -------------------------------------------------------------------------------- /assets/images/.gitkeep: -------------------------------------------------------------------------------- 1 | # Basically just want to ignore the directory contents 2 | -------------------------------------------------------------------------------- /assets/js/admin/admin.js: -------------------------------------------------------------------------------- 1 | // import foo from './bar' 2 | -------------------------------------------------------------------------------- /assets/js/frontend/components/.gitkeep: -------------------------------------------------------------------------------- 1 | # Basically just want to ignore the directory contents 2 | -------------------------------------------------------------------------------- /assets/js/frontend/frontend.js: -------------------------------------------------------------------------------- 1 | // import foo from './components/bar'; 2 | -------------------------------------------------------------------------------- /assets/js/shared/chat.js: -------------------------------------------------------------------------------- 1 | /* global ZammadChat chatOptions:true */ 2 | jQuery(function initForm($) { 3 | const form = $('#fallback-form'); 4 | 5 | /** 6 | * Console debug helper 7 | * 8 | * @param {string} message Message to log in console 9 | * @param {boolean} plain Log without prepending a string 10 | */ 11 | function zammadDebugMessage(message, plain = false) { 12 | if (chatOptions.debug) { 13 | console.log(!plain ? `DEBUG Zammad - WP : ${message}` : message); // eslint-disable-line no-console 14 | } 15 | } 16 | 17 | /** 18 | * Modify chat modal text 19 | * 20 | * @param {string} newText New text displayed in the chat modal, e.g. while waiting for an agent 21 | */ 22 | function zammadChatModalText(newText) { 23 | $('.zammad-chat-modal-text').text(newText); 24 | } 25 | 26 | /** 27 | * Updates the title of the Chat Badge 28 | * 29 | * @param {string} newTitle New label of the chat badge 30 | */ 31 | function zammadChatBadgeTitle(newTitle = null) { 32 | // If no title is passed, use the form title 33 | const badgeTitle = newTitle || form.find('h2').text(); 34 | 35 | // Hide the form title 36 | if (badgeTitle.length > 0) { 37 | form.find('h2').hide(); 38 | 39 | // Update Chat Badge Title 40 | $('.zammad-chat-welcome-text').text(badgeTitle); 41 | } 42 | } 43 | 44 | /** 45 | * Loads the fallback form into the chat window 46 | * 47 | * @param {string} prependToForm String or HTML to prepend to the form 48 | */ 49 | function zammadDisplayFallbackForm(prependToForm = '') { 50 | // Prepend HTML to form 51 | form.prepend(prependToForm); 52 | 53 | // Form button add class 54 | form.find('.btn').addClass('button'); 55 | 56 | $('.zammad-chat-modal').addClass('zammad-fallback-form').html(form); 57 | 58 | // Destroying the chat destroys the open/close functionality 59 | $('.zammad-chat .zammad-chat-header').on('click touchstart', function toggleZammadChat() { 60 | const zammadChat = $(this).parent('.zammad-chat'); 61 | 62 | if (zammadChat.hasClass('zammad-chat-is-open')) { 63 | zammadChat.removeClass('zammad-chat-is-open'); 64 | } else { 65 | zammadChat.addClass('zammad-chat-is-open'); 66 | } 67 | }); 68 | 69 | // Add the form 70 | form.show(); 71 | } 72 | 73 | /** 74 | * Zammad Chat Init 75 | */ 76 | function zammadInitChat() { 77 | if ($.isFunction(window.ZammadChat)) { 78 | const chat = new ZammadChat({ 79 | debug: chatOptions.debug, 80 | title: chatOptions.chatTitle, 81 | fontSize: chatOptions.fontSize, 82 | chatId: chatOptions.chatID, 83 | show: chatOptions.show, 84 | flat: chatOptions.flat, 85 | background: chatOptions.background, 86 | buttonClass: chatOptions.buttonClass, 87 | inactiveClass: chatOptions.inactiveClass, 88 | cssAutoload: chatOptions.cssAutoload, 89 | cssUrl: chatOptions.cssUrl, 90 | }); 91 | 92 | if (chat) { 93 | zammadDebugMessage('Chat is set-up:'); 94 | zammadDebugMessage(chat, true); 95 | zammadDebugMessage( 96 | `Form fallback is turned ${chatOptions.formFallback ? 'on' : 'off'}`, 97 | ); 98 | } 99 | 100 | if (chatOptions.formFallback) { 101 | zammadDebugMessage('Look for the Form.'); 102 | 103 | if (form.length && chat) { 104 | zammadDebugMessage('Fallback Form is available!'); 105 | 106 | // On ERROR: E.g. there is no agent online 107 | chat.onError = () => { 108 | zammadDebugMessage('No agent online. Get the fallback!'); 109 | 110 | // Update Chat Badge Title 111 | zammadChatBadgeTitle(); 112 | 113 | // Show chat badge 114 | chat.show(); 115 | 116 | // Add form on open 117 | chat.onOpenAnimationEnd = () => { 118 | // Close session & destroy to avoid reconnection 119 | chat.sessionClose(); 120 | chat.destroy(); 121 | 122 | // Display Fallback Form 123 | zammadDisplayFallbackForm(chatOptions.formFallbackMessage); 124 | }; 125 | }; 126 | 127 | chat.showLoader = () => { 128 | zammadDebugMessage('Show loader...'); 129 | // Get the Zammad loader view 130 | const loader = chat.el 131 | .find('.zammad-chat-modal') 132 | .html(chat.view('loader')()); 133 | // Allow to override the loading text 134 | if (chatOptions.loaderWaitingMessage.length) { 135 | zammadChatModalText(chatOptions.loaderWaitingMessage); 136 | } 137 | return loader; 138 | }; 139 | 140 | chat.onQueue = () => { 141 | zammadDebugMessage('Waiting for an agent to answer...'); 142 | zammadDebugMessage(chat, true); 143 | 144 | // Reducing the time we wait for an agent to answer 145 | chat.waitingListTimeout.options.timeout = chatOptions.timeout; 146 | chat.waitingListTimeout.options.timeoutIntervallCheck = 147 | chatOptions.timeoutIntervallCheck; 148 | 149 | // Let the user know that we are waiting 150 | zammadChatModalText(chatOptions.waitingListWaitingMessage); 151 | }; 152 | 153 | chat.showWaitingListTimeout = () => { 154 | zammadDebugMessage('No answer, show the form and close connection...'); 155 | 156 | // Display Fallback Form, prepend a message 157 | zammadChatBadgeTitle(); 158 | zammadDisplayFallbackForm(chatOptions.waitingListTimeoutMessage); 159 | 160 | // Add reload functionality e.g. to buttons 161 | form.find('.js-restart').on('click', function zammadReloadWindow() { 162 | zammadDebugMessage('Reload Window to start new chat...'); 163 | window.location.reload(); 164 | }); 165 | 166 | // Close the session 167 | return chat.sessionClose(); 168 | }; 169 | 170 | // Toggle/minimize chat window instead of closing connection 171 | chat.toggle = (event) => { 172 | if (chat.isOpen) { 173 | zammadDebugMessage('Close or Minimize'); 174 | return chat.maybeMinimize(event); 175 | } 176 | zammadDebugMessage('Open'); 177 | return chat.open(event); 178 | }; 179 | 180 | // Adds a minimize functionality 181 | chat.maybeMinimize = (event) => { 182 | zammadDebugMessage( 183 | 'Minimize or close chat window, depending on chat status.', 184 | ); 185 | 186 | // If there is no session ID OR waiting in queue OR no messages yet, we close the window and end connection 187 | if (!chat.sessionId || chat.inQueue || !chat.lastAddedType) { 188 | // Close chat as it is the default 189 | zammadDebugMessage('Close chat.'); 190 | return chat.close(event); 191 | } 192 | 193 | // Otherwise we proceed to minimize 194 | zammadDebugMessage('Minimize running chat.'); 195 | 196 | const remainerHeight = 197 | chat.el.height() - chat.el.find('.zammad-chat-header').outerHeight(); 198 | 199 | return chat.el.animate( 200 | { 201 | bottom: -remainerHeight, 202 | }, 203 | 500, 204 | function minimizeChatWindow() { 205 | chat.el.css('bottom', ''); 206 | chat.el.removeClass('zammad-chat-is-open'); 207 | chat.isOpen = false; 208 | chat.isMinimized = true; 209 | }, 210 | ); 211 | }; 212 | 213 | // Extends function scrollToBottom( to open minimized chat window if new message arrives 214 | chat.originalScrollToBottom = chat.scrollToBottom; 215 | chat.scrollToBottom = (arg) => { 216 | if (chat.isMinimized && !chat.isOpen) { 217 | chat.open(); 218 | } 219 | chat.originalScrollToBottom(arg); 220 | }; 221 | } else if (!form.length) { 222 | zammadDebugMessage('The Fallback Form is missing. Maybe a caching issue?'); 223 | } 224 | } 225 | } else { 226 | // Error: chat.js not available from Zammad 227 | zammadDebugMessage('Zammad Server or remote JS is not available.'); 228 | } 229 | } 230 | 231 | /** 232 | * Build the Chat 233 | */ 234 | zammadInitChat(); 235 | }); 236 | -------------------------------------------------------------------------------- /assets/js/shared/form.js: -------------------------------------------------------------------------------- 1 | /* global formOptions:true */ 2 | jQuery(function initForm($) { 3 | $(formOptions.formElement).ZammadForm({ 4 | messageTitle: formOptions.messageTitle, 5 | messageSubmit: formOptions.messageSubmit, 6 | messageThankYou: formOptions.messageThankYou, 7 | debug: formOptions.debug, 8 | showTitle: formOptions.showTitle, 9 | modal: formOptions.modal, 10 | noCSS: formOptions.noCSS, 11 | attachmentSupport: formOptions.attachmentSupport, 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /assets/js/shared/shared.js: -------------------------------------------------------------------------------- 1 | // import foo from './bar' 2 | -------------------------------------------------------------------------------- /assets/svg/.gitkeep: -------------------------------------------------------------------------------- 1 | # Basically just want to ignore the directory contents 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Babel Config. 3 | * 4 | * @param {Object} api The bable API 5 | * @return {{presets: {Object}}} The babel configuration. 6 | */ 7 | module.exports = (api) => { 8 | /** 9 | * @see https://babeljs.io/docs/en/config-files#apicache 10 | */ 11 | api.cache.using( () => process.env.NODE_ENV === 'development' ); 12 | 13 | /** 14 | * Presets 15 | * 16 | * @see https://babeljs.io/docs/en/presets 17 | * @type {Array} 18 | */ 19 | const presets = [ 20 | [ 21 | '@10up/babel-preset-default', 22 | { 23 | wordpress: true, 24 | }, 25 | ], 26 | ]; 27 | 28 | /** 29 | * Plugins 30 | * 31 | * @see https://babeljs.io/docs/en/plugins 32 | * @type {Array} 33 | */ 34 | const plugins = []; 35 | 36 | return { 37 | presets, 38 | plugins, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ouun/zammad-wp", 3 | "type": "wordpress-plugin", 4 | "license": "GPL-2.0-or-later", 5 | "description": "Integrates Zammad Helpdesk into WordPress", 6 | "homepage": "https://ouun.io", 7 | "authors": [ 8 | { 9 | "name": "Philipp Wellmer", 10 | "email": "philipp@ouun.io" 11 | } 12 | ], 13 | "keywords": ["wordpress", "wordpress-block", "zammad", "helpdesk", "livechat", "support"], 14 | "repositories": [ 15 | { 16 | "type": "vcs", 17 | "url": "git@github.com:ouun/zammad-wp-pro.git" 18 | } 19 | ], 20 | "autoload": { 21 | "psr-4": { 22 | "ZammadWp\\": "includes/classes/" 23 | } 24 | }, 25 | "require": { 26 | "php": ">=7.0 < 9.0", 27 | "zammad/zammad-api-client-php": "^2.0" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^9.5", 31 | "10up/wp_mock": "dev-trunk", 32 | "10up/phpcs-composer": "dev-master", 33 | "wp-coding-standards/wpcs": "*" 34 | }, 35 | "scripts": { 36 | "lint": "phpcs . -s", 37 | "lint-fix": "phpcbf ." 38 | }, 39 | "config": { 40 | "allow-plugins": { 41 | "dealerdirect/phpcodesniffer-composer-installer": true 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /config/webpack.config.common.js: -------------------------------------------------------------------------------- 1 | const path = require( 'path' ); 2 | const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); 3 | const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); 4 | const FixStyleOnlyEntriesPlugin = require( 'webpack-fix-style-only-entries' ); 5 | const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); 6 | const StyleLintPlugin = require( 'stylelint-webpack-plugin' ); 7 | const WebpackBar = require( 'webpackbar' ); 8 | const ImageminPlugin = require( 'imagemin-webpack-plugin' ).default; 9 | 10 | const isProduction = process.env.NODE_ENV === 'production'; 11 | 12 | // Config files. 13 | const settings = require( './webpack.settings.js' ); 14 | 15 | /** 16 | * Configure entries. 17 | * 18 | * @return {Object[]} Array of webpack settings. 19 | */ 20 | const configureEntries = () => { 21 | const entries = {}; 22 | 23 | for (const [key, value] of Object.entries( settings.entries )) { 24 | entries[key] = path.resolve( process.cwd(), value ); 25 | } 26 | 27 | return entries; 28 | }; 29 | 30 | module.exports = { 31 | entry: configureEntries(), 32 | output: { 33 | path: path.resolve( process.cwd(), settings.paths.dist.base ), 34 | filename: settings.filename.js, 35 | /** 36 | * If multiple webpack runtimes (from different compilations) are used on the same webpage, 37 | * there is a risk of conflicts of on-demand chunks in the global namespace. 38 | * 39 | * @see (@link https://webpack.js.org/configuration/output/#outputjsonpfunction) 40 | */ 41 | jsonpFunction: '__ZammadWp_webpackJsonp', 42 | }, 43 | 44 | // Console stats output. 45 | // @link https://webpack.js.org/configuration/stats/#stats 46 | stats: settings.stats, 47 | 48 | // External objects. 49 | externals: { 50 | jquery: 'jQuery', 51 | lodash: 'lodash', 52 | }, 53 | 54 | // Performance settings. 55 | performance: { 56 | maxAssetSize: settings.performance.maxAssetSize, 57 | }, 58 | 59 | // Build rules to handle asset files. 60 | module: { 61 | rules: [ 62 | // Lint JS. 63 | { 64 | test: /\.js$/, 65 | enforce: 'pre', 66 | loader: 'eslint-loader', 67 | options: { 68 | fix: true, 69 | }, 70 | }, 71 | 72 | // Scripts. 73 | { 74 | test: /\.js$/, 75 | exclude: /node_modules(?!\/@10up)/, 76 | use: [ 77 | { 78 | loader: 'babel-loader', 79 | options: { 80 | cacheDirectory: true, 81 | sourceMap: ! isProduction, 82 | }, 83 | }, 84 | ], 85 | }, 86 | 87 | // Styles. 88 | { 89 | test: /\.(s*)css$/, 90 | include: path.resolve( process.cwd(), settings.paths.src.css ), 91 | use: [ 92 | { 93 | loader: MiniCssExtractPlugin.loader, 94 | }, 95 | { 96 | loader: 'css-loader', 97 | options: { 98 | sourceMap: ! isProduction, 99 | // We copy fonts etc. using CopyWebpackPlugin. 100 | url: false, 101 | }, 102 | }, 103 | { 104 | loader: 'postcss-loader', 105 | options: { 106 | sourceMap: ! isProduction, 107 | }, 108 | }, 109 | { 110 | loader: 'sass-loader', 111 | options: { 112 | sourceMap: ! isProduction, 113 | }, 114 | }, 115 | ], 116 | }, 117 | ], 118 | }, 119 | 120 | plugins: [ 121 | // Remove the extra JS files Webpack creates for CSS entries. 122 | // This should be fixed in Webpack 5. 123 | new FixStyleOnlyEntriesPlugin( 124 | { 125 | silent: true, 126 | } 127 | ), 128 | 129 | // Clean the `dist` folder on build. 130 | new CleanWebpackPlugin( 131 | { 132 | cleanStaleWebpackAssets: false, 133 | } 134 | ), 135 | 136 | // Extract CSS into individual files. 137 | new MiniCssExtractPlugin( 138 | { 139 | filename: settings.filename.css, 140 | chunkFilename: '[id].css', 141 | } 142 | ), 143 | 144 | // Copy static assets to the `dist` folder. 145 | new CopyWebpackPlugin( 146 | [ 147 | { 148 | from: settings.copyWebpackConfig.from, 149 | to: settings.copyWebpackConfig.to, 150 | context: path.resolve( process.cwd(), settings.paths.src.base ), 151 | }, 152 | ] 153 | ), 154 | 155 | // Compress images 156 | // Must happen after CopyWebpackPlugin 157 | new ImageminPlugin( 158 | { 159 | disable: ! isProduction, 160 | test: settings.ImageminPlugin.test, 161 | } 162 | ), 163 | 164 | // Lint CSS. 165 | new StyleLintPlugin( 166 | { 167 | context: path.resolve( process.cwd(), settings.paths.src.css ), 168 | files: '**/*.css', 169 | } 170 | ), 171 | 172 | // Fancy WebpackBar. 173 | new WebpackBar(), 174 | ], 175 | }; 176 | -------------------------------------------------------------------------------- /config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require( 'webpack-merge' ); // eslint-disable-line import/no-extraneous-dependencies 2 | const BrowserSyncPlugin = require( 'browser-sync-webpack-plugin' ); // eslint-disable-line import/no-extraneous-dependencies 3 | const common = require( './webpack.config.common.js' ); 4 | 5 | // Config files. 6 | const settings = require( './webpack.settings.js' ); 7 | 8 | module.exports = merge( 9 | common, 10 | { 11 | mode: 'development', 12 | devtool: 'inline-cheap-module-source-map', 13 | plugins: [ 14 | // Run BrowserSync. 15 | new BrowserSyncPlugin( 16 | { 17 | host: settings.BrowserSyncConfig.host, 18 | port: settings.BrowserSyncConfig.port, 19 | proxy: settings.BrowserSyncConfig.proxy, 20 | open: settings.BrowserSyncConfig.open, 21 | files: settings.BrowserSyncConfig.files, 22 | }, 23 | { 24 | injectCss: true, 25 | reload: false, 26 | }, 27 | ), 28 | ], 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require( 'webpack-merge' ); 2 | const TerserPlugin = require( 'terser-webpack-plugin' ); // eslint-disable-line import/no-extraneous-dependencies 3 | const common = require( './webpack.config.common.js' ); 4 | 5 | module.exports = merge( 6 | common, 7 | { 8 | mode: 'production', 9 | 10 | optimization: { 11 | minimizer: [ 12 | new TerserPlugin( 13 | { 14 | cache: true, 15 | parallel: true, 16 | sourceMap: false, 17 | terserOptions: { 18 | parse: { 19 | // We want terser to parse ecma 8 code. However, we don't want it 20 | // to apply any minfication steps that turns valid ecma 5 code 21 | // into invalid ecma 5 code. This is why the 'compress' and 'output' 22 | // sections only apply transformations that are ecma 5 safe 23 | // https://github.com/facebook/create-react-app/pull/4234 24 | ecma: 8, 25 | }, 26 | compress: { 27 | ecma: 5, 28 | warnings: false, 29 | // Disabled because of an issue with Uglify breaking seemingly valid code: 30 | // https://github.com/facebook/create-react-app/issues/2376 31 | // Pending further investigation: 32 | // https://github.com/mishoo/UglifyJS2/issues/2011 33 | comparisons: false, 34 | // Disabled because of an issue with Terser breaking valid code: 35 | // https://github.com/facebook/create-react-app/issues/5250 36 | // Pending futher investigation: 37 | // https://github.com/terser-js/terser/issues/120 38 | inline: 2, 39 | }, 40 | output: { 41 | ecma: 5, 42 | comments: false, 43 | }, 44 | ie8: false, 45 | }, 46 | } 47 | ), 48 | ], 49 | }, 50 | } 51 | ); 52 | -------------------------------------------------------------------------------- /config/webpack.settings.js: -------------------------------------------------------------------------------- 1 | // Webpack settings exports. 2 | module.exports = { 3 | entries: { 4 | // JS files. 5 | chat: './assets/js/shared/chat.js', 6 | form: './assets/js/shared/form.js', 7 | 8 | // CSS files. 9 | 'chat-style': './assets/css/shared/chat.scss', 10 | 'form-style': './assets/css/shared/form.scss', 11 | style: './assets/css/frontend/style.css', 12 | }, 13 | filename: { 14 | js: 'js/[name].js', 15 | css: 'css/[name].css', 16 | }, 17 | paths: { 18 | src: { 19 | base: './assets/', 20 | css: './assets/css/', 21 | js: './assets/js/', 22 | }, 23 | dist: { 24 | base: './dist/', 25 | clean: ['./images', './css', './js'], 26 | }, 27 | }, 28 | stats: { 29 | // Copied from `'minimal'`. 30 | all: false, 31 | errors: true, 32 | maxModules: 0, 33 | modules: true, 34 | warnings: true, 35 | // Our additional options. 36 | assets: true, 37 | errorDetails: true, 38 | excludeAssets: /\.(jpe?g|png|gif|svg|woff|woff2)$/i, 39 | moduleTrace: true, 40 | performance: true, 41 | }, 42 | copyWebpackConfig: { 43 | from: '**/*.{jpg,jpeg,png,gif,svg,eot,ttf,woff,woff2}', 44 | to: '[path][name].[ext]', 45 | }, 46 | ImageminPlugin: { 47 | test: /\.(jpe?g|png|gif|svg)$/i, 48 | }, 49 | BrowserSyncConfig: { 50 | host: 'localhost', 51 | port: 1983, 52 | proxy: 'https://maticacorp.test', 53 | open: false, 54 | files: [ 55 | '**/*.php', 56 | 'dist/js/**/*.js', 57 | 'dist/css/**/*.css', 58 | 'dist/svg/**/*.svg', 59 | 'dist/images/**/*.{jpg,jpeg,png,gif}', 60 | 'dist/fonts/**/*.{eot,ttf,woff,woff2,svg}', 61 | ], 62 | }, 63 | performance: { 64 | maxAssetSize: 100000, 65 | }, 66 | manifestConfig: { 67 | basePath: '', 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /dist/css/chat-style.css: -------------------------------------------------------------------------------- 1 | .zammad-chat{border-radius:5px 5px 0 0;bottom:0;-webkit-box-shadow:0 3px 10px rgba(0,0,0,.3);box-shadow:0 3px 10px rgba(0,0,0,.3);color:#000;display:none;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;font-size:9pt;height:3.5em;position:fixed;right:30px;width:33em;will-change:bottom;z-index:999}@media only screen and (max-width:768px){.zammad-chat{border-radius:0!important;font-size:1pc;right:0!important;width:100%}}.zammad-chat--animate{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s}.zammad-chat.zammad-chat-is-loaded{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:0}.zammad-chat.zammad-chat-is-shown{opacity:1}.zammad-chat.zammad-chat-is-shown:not(.zammad-chat-is-open) .zammad-chat-modal-text{display:none}.zammad-chat.zammad-chat-is-open{height:30em}@media only screen and (max-width:768px){.zammad-chat.zammad-chat-is-open{height:100%}}.zammad-chat-icon{fill:currentColor;height:2em;margin-right:5px;margin-top:4px;vertical-align:top;width:2em}.zammad-chat-header{background:#379ad7;border-radius:5px 5px 0 0;-webkit-box-shadow:0 -1px rgba(0,0,0,.1),0 1px hsla(0,0%,100%,.3) inset,0 -1px rgba(0,0,0,.1) inset,0 1px 1px rgba(0,0,0,.13);box-shadow:0 -1px rgba(0,0,0,.1),inset 0 1px hsla(0,0%,100%,.3),inset 0 -1px rgba(0,0,0,.1),0 1px 1px rgba(0,0,0,.13);color:#fff;cursor:pointer;height:3.5em;line-height:2.5em;overflow:hidden;padding:.5em 2.5em .5em 1em;position:relative}@media only screen and (max-width:768px){.zammad-chat-header{border-radius:0!important}}.zammad-chat.zammad-chat-is-open .zammad-chat-header{cursor:default}.zammad-chat-welcome-text{font-size:1.2em}.zammad-chat-header-icon{cursor:pointer;height:100%;line-height:3.4em;position:absolute;right:0;text-align:center;top:0;width:3.4em}.zammad-chat-header-icon:before{content:"";display:inline-block}.zammad-chat-header-icon-close,.zammad-chat-header-icon-open{fill:currentColor;height:auto;vertical-align:middle;width:1.6em}.zammad-chat-header-icon-close{width:1.3em}.zammad-chat-header-icon-close,.zammad-chat.zammad-chat-is-open .zammad-chat-header-icon-open{display:none}.zammad-chat.zammad-chat-is-open .zammad-chat-header-icon-close{display:inline}.zammad-chat-agent{float:left}.zammad-chat-header-controls{float:right}.zammad-chat-agent-avatar{border-radius:100%;float:left;margin-right:.6em;width:2.5em}.zammad-chat-agent-name{font-weight:700}.zammad-chat-agent-status{background:rgba(0,0,0,.1);border-radius:1em;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.04) inset;box-shadow:inset 0 0 0 1px rgba(0,0,0,.04);display:inline-block;line-height:2em;margin:.25em 1em;padding:0 .7em}.zammad-chat-agent-status:before{background:#f35912;border-radius:100%;-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.04) inset;box-shadow:inset 0 0 0 1px rgba(0,0,0,.04);content:"";display:inline-block;height:.9em;margin-right:.3em;position:relative;vertical-align:middle;width:.9em}.zammad-chat-agent-status[data-status=online]:before{background:#52c782}.zammad-chat-agent-status[data-status=connecting]:before{-webkit-animation:connect-fade .6s linear infinite alternate;animation:connect-fade .6s linear infinite alternate;background:#faab00}@-webkit-keyframes connect-fade{0%{opacity:.5;-webkit-transform:scale(.6);transform:scale(.6)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes connect-fade{0%{opacity:.5;-webkit-transform:scale(.6);transform:scale(.6)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.zammad-chat-is-open .zammad-chat-modal{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.zammad-chat-modal{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#fff;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;left:0;margin-top:1px;padding:20px;position:absolute;right:0;text-align:center;top:3.5em;z-index:1}.zammad-chat-modal:empty{display:none}.zammad-chat-modal.zammad-fallback-form{overflow:auto}.zammad-chat-modal.zammad-fallback-form #fallback-form{height:100%;width:100%}.zammad-chat-modal.zammad-fallback-form #fallback-form .zammad-form{padding:15px 0 30px 0}.zammad-chat-modal-text{font-size:1.3em;line-height:1.45}.zammad-chat-modal-text .zammad-chat-loading-animation{font-size:.7em}.zammad-chat-modal-text .zammad-chat-button{font-size:.8em;margin-top:1em}.zammad-chat-modal .zammad-chat-loading-animation{margin-bottom:8px;vertical-align:middle}.zammad-scroll-hint{-webkit-box-align:center;-ms-flex-align:center;align-items:center;background:#f9fafa;border-bottom:1px solid #e8e8e8;color:#999;cursor:pointer;display:-webkit-box;display:-ms-flexbox;display:flex;padding:7px 10px 6px}.zammad-scroll-hint.is-hidden{display:none}.zammad-scroll-hint-icon{fill:#c4c7ca;margin-right:8px}.zammad-chat-body{background:#fff;display:none;-webkit-box-flex:1;-ms-flex:1;flex:1;overflow:auto;-webkit-overflow-scrolling:touch;-ms-scroll-chaining:none;overscroll-behavior:contain;padding:.5em 1em}@media only screen and (max-width:768px){.zammad-chat-body{-webkit-box-flex:1;-ms-flex:1;flex:1;height:auto}}.zammad-chat-is-open .zammad-chat-body{display:block}.zammad-chat-timestamp{color:#999;font-size:.9em}.zammad-chat-status,.zammad-chat-timestamp{margin:1em 0;text-align:center}.zammad-chat-message{margin:.5em 0}.zammad-chat-message-body{border-radius:1em;word-wrap:break-word}.zammad-chat-message-body,.zammad-chat-status-inner{background:#ededed;-webkit-box-shadow:0 2px hsla(0,0%,100%,.15) inset,0 0 0 1px rgba(0,0,0,.08) inset,0 1px rgba(0,0,0,.02);box-shadow:inset 0 2px hsla(0,0%,100%,.15),inset 0 0 0 1px rgba(0,0,0,.08),0 1px rgba(0,0,0,.02);display:inline-block;line-height:1.4;max-width:70%;padding:.5em 1em}.zammad-chat-status-inner{background:#eee;border-radius:.5em}.zammad-chat-message--customer{text-align:right}.zammad-chat-message--agent+.zammad-chat-message--customer,.zammad-chat-message--customer+.zammad-chat-message--agent{margin-top:1em}.zammad-chat-message--customer .zammad-chat-message-body{background:#379ad7;color:#fff}.zammad-chat-message--unread{font-weight:700}.zammad-chat-loading-animation{display:inline-block}.zammad-chat-loading-circle{-webkit-animation:load-fade .6s ease-in-out infinite alternate;animation:load-fade .6s ease-in-out infinite alternate;background:#d9d9d9;border-radius:100%;display:inline-block;height:.55em;width:.55em}.zammad-chat-loading-circle+.zammad-chat-loading-circle{-webkit-animation-delay:.13s;animation-delay:.13s}.zammad-chat-loading-circle+.zammad-chat-loading-circle+.zammad-chat-loading-circle{-webkit-animation-delay:.26s;animation-delay:.26s}@-webkit-keyframes load-fade{0%{opacity:.5;-webkit-transform:scale(.6);transform:scale(.6)}67%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes load-fade{0%{opacity:.5;-webkit-transform:scale(.6);transform:scale(.6)}67%{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.zammad-chat-controls{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;background:#fff;border-top:1px solid #ededed;-webkit-box-shadow:0 1px rgba(0,0,0,.01),0 -1px rgba(0,0,0,.02);box-shadow:0 1px rgba(0,0,0,.01),0 -1px rgba(0,0,0,.02);display:none;line-height:1.4em;margin:0;overflow:hidden;padding:0;position:relative}.zammad-chat-is-open .zammad-chat-controls{display:-webkit-box;display:-ms-flexbox;display:flex}.zammad-chat-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:none;-webkit-box-shadow:none;box-shadow:none;-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-box-flex:1;-ms-flex:1;flex:1;float:left;font-family:inherit;font-size:inherit;line-height:1.4em;margin:0;max-height:6em;min-height:1.4em;outline:none;overflow:auto;padding:1em 2em}.zammad-chat-input::-webkit-input-placeholder{color:#d9d9d9}.zammad-chat-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#379ad7;border:none;border-radius:1.5em;-webkit-box-shadow:0 2px hsla(0,0%,100%,.25) inset,0 0 0 1px rgba(0,0,0,.1) inset,0 1px rgba(0,0,0,.1);box-shadow:inset 0 2px hsla(0,0%,100%,.25),inset 0 0 0 1px rgba(0,0,0,.1),0 1px rgba(0,0,0,.1);color:#fff;cursor:pointer;display:inline-block;font-family:inherit;font-size:inherit;line-height:normal;margin:.63em 1em;outline:none;padding:.5em 1.2em}.zammad-chat-send{float:right}.zammad-chat-button:disabled,.zammad-chat-input:disabled{opacity:.3}.zammad-chat-is-hidden{display:none}.zammad-chat--flat .zammad-chat-body,.zammad-chat--flat .zammad-chat-header{border:none}.zammad-chat--flat .zammad-chat-agent-status,.zammad-chat--flat .zammad-chat-button,.zammad-chat--flat .zammad-chat-header,.zammad-chat--flat .zammad-chat-message-body,.zammad-chat--flat .zammad-chat-status{-webkit-box-shadow:none;box-shadow:none} 2 | -------------------------------------------------------------------------------- /dist/css/form-style.css: -------------------------------------------------------------------------------- 1 | .zammad-form{text-align:left;width:100%}.zammad-form h1,.zammad-form h2{margin-top:0}.zammad-form .form-group{margin-bottom:15px}.zammad-form .form-control{display:block;width:100%}.zammad-form .has-error .form-control{border-color:#a94442}.zammad-form .has-error label{color:#a94442}.zammad-form .js-thankyou{text-align:center}.zammad-form-modal{height:100%;left:0;position:fixed;text-align:center;top:0;width:100%;z-index:999}.zammad-form-modal:before{content:"";display:inline-block;height:100%;margin-right:-.25em;vertical-align:middle}.zammad-form-modal-backdrop{background-color:grey;height:100%;left:0;opacity:.7;position:absolute;top:0;width:100%}.zammad-form-modal-body{background:#fff;-webkit-box-shadow:0 0 50px rgba(0,0,0,.3);box-shadow:0 0 50px rgba(0,0,0,.3);-webkit-box-sizing:border-box;box-sizing:border-box;color:#949494;display:inline-block;margin:0 auto;max-width:26em;padding:24px 24px 22px;position:relative;text-align:left;vertical-align:middle;width:90%} 2 | -------------------------------------------------------------------------------- /dist/css/style.css: -------------------------------------------------------------------------------- 1 | :root{--c-black:#000;--c-white:#fff} 2 | -------------------------------------------------------------------------------- /dist/js/chat.js: -------------------------------------------------------------------------------- 1 | !function(t){var n={};function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}e.m=t,e.c=n,e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{enumerable:!0,get:r})},e.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,n){if(1&n&&(t=e(t)),8&n)return t;if(4&n&&"object"===typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(e.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&n&&"string"!=typeof t)for(var o in t)e.d(r,o,function(n){return t[n]}.bind(null,o));return r},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},e.p="",e(e.s=37)}([function(t,n,e){(function(n){var e=function(t){return t&&t.Math==Math&&t};t.exports=e("object"==typeof globalThis&&globalThis)||e("object"==typeof window&&window)||e("object"==typeof self&&self)||e("object"==typeof n&&n)||Function("return this")()}).call(this,e(39))},function(t,n){var e={}.hasOwnProperty;t.exports=function(t,n){return e.call(t,n)}},function(t,n,e){var r=e(3);t.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},function(t,n){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,n){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},function(t,n,e){var r=e(2),o=e(6),i=e(16);t.exports=r?function(t,n,e){return o.f(t,n,i(1,e))}:function(t,n,e){return t[n]=e,t}},function(t,n,e){var r=e(2),o=e(21),i=e(7),c=e(20),a=Object.defineProperty;n.f=r?a:function(t,n,e){if(i(t),n=c(n,!0),i(e),o)try{return a(t,n,e)}catch(t){}if("get"in e||"set"in e)throw TypeError("Accessors not supported");return"value"in e&&(t[n]=e.value),t}},function(t,n,e){var r=e(4);t.exports=function(t){if(!r(t))throw TypeError(String(t)+" is not an object");return t}},function(t,n,e){var r=e(12),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,n,e){var r=e(17),o=e(19);t.exports=function(t){return r(o(t))}},function(t,n,e){var r=e(0),o=e(5);t.exports=function(t,n){try{o(r,t,n)}catch(e){r[t]=n}return n}},function(t,n){t.exports={}},function(t,n){var e=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:e)(t)}},function(t,n){t.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},function(t,n,e){var r=e(0),o=e(15).f,i=e(5),c=e(41),a=e(10),u=e(45),f=e(52);t.exports=function(t,n){var e,s,l,p,h,d=t.target,v=t.global,m=t.stat;if(e=v?r:m?r[d]||a(d,{}):(r[d]||{}).prototype)for(s in n){if(p=n[s],l=t.noTargetGet?(h=o(e,s))&&h.value:e[s],!f(v?s:d+(m?".":"#")+s,t.forced)&&void 0!==l){if(typeof p===typeof l)continue;u(p,l)}(t.sham||l&&l.sham)&&i(p,"sham",!0),c(e,s,p,t)}}},function(t,n,e){var r=e(2),o=e(40),i=e(16),c=e(9),a=e(20),u=e(1),f=e(21),s=Object.getOwnPropertyDescriptor;n.f=r?s:function(t,n){if(t=c(t),n=a(n,!0),f)try{return s(t,n)}catch(t){}if(u(t,n))return i(!o.f.call(t,n),t[n])}},function(t,n){t.exports=function(t,n){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:n}}},function(t,n,e){var r=e(3),o=e(18),i="".split;t.exports=r((function(){return!Object("z").propertyIsEnumerable(0)}))?function(t){return"String"==o(t)?i.call(t,""):Object(t)}:Object},function(t,n){var e={}.toString;t.exports=function(t){return e.call(t).slice(8,-1)}},function(t,n){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,n,e){var r=e(4);t.exports=function(t,n){if(!r(t))return t;var e,o;if(n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;if("function"==typeof(e=t.valueOf)&&!r(o=e.call(t)))return o;if(!n&&"function"==typeof(e=t.toString)&&!r(o=e.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,n,e){var r=e(2),o=e(3),i=e(22);t.exports=!r&&!o((function(){return 7!=Object.defineProperty(i("div"),"a",{get:function(){return 7}}).a}))},function(t,n,e){var r=e(0),o=e(4),i=r.document,c=o(i)&&o(i.createElement);t.exports=function(t){return c?i.createElement(t):{}}},function(t,n,e){var r=e(24),o=Function.toString;"function"!=typeof r.inspectSource&&(r.inspectSource=function(t){return o.call(t)}),t.exports=r.inspectSource},function(t,n,e){var r=e(0),o=e(10),i=r["__core-js_shared__"]||o("__core-js_shared__",{});t.exports=i},function(t,n,e){var r=e(26),o=e(27),i=r("keys");t.exports=function(t){return i[t]||(i[t]=o(t))}},function(t,n,e){var r=e(44),o=e(24);(t.exports=function(t,n){return o[t]||(o[t]=void 0!==n?n:{})})("versions",[]).push({version:"3.6.5",mode:r?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},function(t,n){var e=0,r=Math.random();t.exports=function(t){return"Symbol("+String(void 0===t?"":t)+")_"+(++e+r).toString(36)}},function(t,n,e){var r=e(47),o=e(0),i=function(t){return"function"==typeof t?t:void 0};t.exports=function(t,n){return arguments.length<2?i(r[t])||i(o[t]):r[t]&&r[t][n]||o[t]&&o[t][n]}},function(t,n,e){var r=e(1),o=e(9),i=e(49).indexOf,c=e(11);t.exports=function(t,n){var e,a=o(t),u=0,f=[];for(e in a)!r(c,e)&&r(a,e)&&f.push(e);for(;n.length>u;)r(a,e=n[u++])&&(~i(f,e)||f.push(e));return f}},function(t,n,e){var r=e(54);t.exports=function(t,n,e){if(r(t),void 0===n)return t;switch(e){case 0:return function(){return t.call(n)};case 1:return function(e){return t.call(n,e)};case 2:return function(e,r){return t.call(n,e,r)};case 3:return function(e,r,o){return t.call(n,e,r,o)}}return function(){return t.apply(n,arguments)}}},function(t,n,e){var r=e(19);t.exports=function(t){return Object(r(t))}},function(t,n,e){var r=e(4),o=e(33),i=e(34)("species");t.exports=function(t,n){var e;return o(t)&&("function"!=typeof(e=t.constructor)||e!==Array&&!o(e.prototype)?r(e)&&null===(e=e[i])&&(e=void 0):e=void 0),new(void 0===e?Array:e)(0===n?0:n)}},function(t,n,e){var r=e(18);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,n,e){var r=e(0),o=e(26),i=e(1),c=e(27),a=e(35),u=e(55),f=o("wks"),s=r.Symbol,l=u?s:s&&s.withoutSetter||c;t.exports=function(t){return i(f,t)||(a&&i(s,t)?f[t]=s[t]:f[t]=l("Symbol."+t)),f[t]}},function(t,n,e){var r=e(3);t.exports=!!Object.getOwnPropertySymbols&&!r((function(){return!String(Symbol())}))},function(t,n,e){var r=e(34),o=e(56),i=e(6),c=r("unscopables"),a=Array.prototype;void 0==a[c]&&i.f(a,c,{configurable:!0,value:o(null)}),t.exports=function(t){a[c][t]=!0}},function(t,n,e){"use strict";e.r(n);e(38),e(61),e(63);jQuery((function(t){var n=t("#fallback-form");function e(t){var n=arguments.length>1&&void 0!==arguments[1]&&arguments[1];chatOptions.debug&&console.log(n?t:"DEBUG Zammad - WP : ".concat(t))}function r(n){t(".zammad-chat-modal-text").text(n)}function o(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,r=e||n.find("h2").text();r.length>0&&(n.find("h2").hide(),t(".zammad-chat-welcome-text").text(r))}function i(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";n.prepend(e),n.find(".btn").addClass("button"),t(".zammad-chat-modal").addClass("zammad-fallback-form").html(n),t(".zammad-chat .zammad-chat-header").on("click touchstart",(function(){var n=t(this).parent(".zammad-chat");n.hasClass("zammad-chat-is-open")?n.removeClass("zammad-chat-is-open"):n.addClass("zammad-chat-is-open")})),n.show()}!function(){if(t.isFunction(window.ZammadChat)){var c=new ZammadChat({debug:chatOptions.debug,title:chatOptions.chatTitle,fontSize:chatOptions.fontSize,chatId:chatOptions.chatID,show:chatOptions.show,flat:chatOptions.flat,background:chatOptions.background,buttonClass:chatOptions.buttonClass,inactiveClass:chatOptions.inactiveClass,cssAutoload:chatOptions.cssAutoload,cssUrl:chatOptions.cssUrl});c&&(e("Chat is set-up:"),e(c,!0),e("Form fallback is turned ".concat(chatOptions.formFallback?"on":"off"))),chatOptions.formFallback&&(e("Look for the Form."),n.length&&c?(e("Fallback Form is available!"),c.onError=function(){e("No agent online. Get the fallback!"),o(),c.show(),c.onOpenAnimationEnd=function(){c.sessionClose(),c.destroy(),i(chatOptions.formFallbackMessage)}},c.showLoader=function(){e("Show loader...");var t=c.el.find(".zammad-chat-modal").html(c.view("loader")());return chatOptions.loaderWaitingMessage.length&&r(chatOptions.loaderWaitingMessage),t},c.onQueue=function(){e("Waiting for an agent to answer..."),e(c,!0),c.waitingListTimeout.options.timeout=chatOptions.timeout,c.waitingListTimeout.options.timeoutIntervallCheck=chatOptions.timeoutIntervallCheck,r(chatOptions.waitingListWaitingMessage)},c.showWaitingListTimeout=function(){return e("No answer, show the form and close connection..."),o(),i(chatOptions.waitingListTimeoutMessage),n.find(".js-restart").on("click",(function(){e("Reload Window to start new chat..."),window.location.reload()})),c.sessionClose()},c.toggle=function(t){return c.isOpen?(e("Close or Minimize"),c.maybeMinimize(t)):(e("Open"),c.open(t))},c.maybeMinimize=function(t){if(e("Minimize or close chat window, depending on chat status."),!c.sessionId||c.inQueue||!c.lastAddedType)return e("Close chat."),c.close(t);e("Minimize running chat.");var n=c.el.height()-c.el.find(".zammad-chat-header").outerHeight();return c.el.animate({bottom:-n},500,(function(){c.el.css("bottom",""),c.el.removeClass("zammad-chat-is-open"),c.isOpen=!1,c.isMinimized=!0}))},c.originalScrollToBottom=c.scrollToBottom,c.scrollToBottom=function(t){c.isMinimized&&!c.isOpen&&c.open(),c.originalScrollToBottom(t)}):n.length||e("The Fallback Form is missing. Maybe a caching issue?"))}else e("Zammad Server or remote JS is not available.")}()}))},function(t,n,e){"use strict";var r=e(14),o=e(53).find,i=e(36),c=e(60),a=!0,u=c("find");"find"in[]&&Array(1).find((function(){a=!1})),r({target:"Array",proto:!0,forced:a||!u},{find:function(t){return o(this,t,arguments.length>1?arguments[1]:void 0)}}),i("find")},function(t,n){var e;e=function(){return this}();try{e=e||new Function("return this")()}catch(t){"object"===typeof window&&(e=window)}t.exports=e},function(t,n,e){"use strict";var r={}.propertyIsEnumerable,o=Object.getOwnPropertyDescriptor,i=o&&!r.call({1:2},1);n.f=i?function(t){var n=o(this,t);return!!n&&n.enumerable}:r},function(t,n,e){var r=e(0),o=e(5),i=e(1),c=e(10),a=e(23),u=e(42),f=u.get,s=u.enforce,l=String(String).split("String");(t.exports=function(t,n,e,a){var u=!!a&&!!a.unsafe,f=!!a&&!!a.enumerable,p=!!a&&!!a.noTargetGet;"function"==typeof e&&("string"!=typeof n||i(e,"name")||o(e,"name",n),s(e).source=l.join("string"==typeof n?n:"")),t!==r?(u?!p&&t[n]&&(f=!0):delete t[n],f?t[n]=e:o(t,n,e)):f?t[n]=e:c(n,e)})(Function.prototype,"toString",(function(){return"function"==typeof this&&f(this).source||a(this)}))},function(t,n,e){var r,o,i,c=e(43),a=e(0),u=e(4),f=e(5),s=e(1),l=e(25),p=e(11),h=a.WeakMap;if(c){var d=new h,v=d.get,m=d.has,y=d.set;r=function(t,n){return y.call(d,t,n),n},o=function(t){return v.call(d,t)||{}},i=function(t){return m.call(d,t)}}else{var g=l("state");p[g]=!0,r=function(t,n){return f(t,g,n),n},o=function(t){return s(t,g)?t[g]:{}},i=function(t){return s(t,g)}}t.exports={set:r,get:o,has:i,enforce:function(t){return i(t)?o(t):r(t,{})},getterFor:function(t){return function(n){var e;if(!u(n)||(e=o(n)).type!==t)throw TypeError("Incompatible receiver, "+t+" required");return e}}}},function(t,n,e){var r=e(0),o=e(23),i=r.WeakMap;t.exports="function"===typeof i&&/native code/.test(o(i))},function(t,n){t.exports=!1},function(t,n,e){var r=e(1),o=e(46),i=e(15),c=e(6);t.exports=function(t,n){for(var e=o(n),a=c.f,u=i.f,f=0;fs;)if((a=u[s++])!=a)return!0}else for(;f>s;s++)if((t||s in u)&&u[s]===e)return t||s||0;return!t&&-1}};t.exports={includes:c(!0),indexOf:c(!1)}},function(t,n,e){var r=e(12),o=Math.max,i=Math.min;t.exports=function(t,n){var e=r(t);return e<0?o(e+n,0):i(e,n)}},function(t,n){n.f=Object.getOwnPropertySymbols},function(t,n,e){var r=e(3),o=/#|\.prototype\./,i=function(t,n){var e=a[c(t)];return e==f||e!=u&&("function"==typeof n?r(n):!!n)},c=i.normalize=function(t){return String(t).replace(o,".").toLowerCase()},a=i.data={},u=i.NATIVE="N",f=i.POLYFILL="P";t.exports=i},function(t,n,e){var r=e(30),o=e(17),i=e(31),c=e(8),a=e(32),u=[].push,f=function(t){var n=1==t,e=2==t,f=3==t,s=4==t,l=6==t,p=5==t||l;return function(h,d,v,m){for(var y,g,b=i(h),x=o(b),O=r(d,v,3),w=c(x.length),S=0,j=m||a,C=n?j(h,w):e?j(h,0):void 0;w>S;S++)if((p||S in x)&&(g=O(y=x[S],S,b),t))if(n)C[S]=g;else if(g)switch(t){case 3:return!0;case 5:return y;case 6:return S;case 2:u.call(C,y)}else if(s)return!1;return l?-1:f||s?s:C}};t.exports={forEach:f(0),map:f(1),filter:f(2),some:f(3),every:f(4),find:f(5),findIndex:f(6)}},function(t,n){t.exports=function(t){if("function"!=typeof t)throw TypeError(String(t)+" is not a function");return t}},function(t,n,e){var r=e(35);t.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},function(t,n,e){var r,o=e(7),i=e(57),c=e(13),a=e(11),u=e(59),f=e(22),s=e(25),l=s("IE_PROTO"),p=function(){},h=function(t){return"):', " $script_execution", $tag, 1); 379 | } 380 | 381 | return $tag; 382 | } 383 | -------------------------------------------------------------------------------- /includes/functions/form.php: -------------------------------------------------------------------------------- 1 | $form_element, 44 | 'debug' => false, 45 | 'showTitle' => true, 46 | 'messageTitle' => __('Contact us', 'zammad-wp'), 47 | 'messageSubmit' => __('Submit', 'zammad-wp'), 48 | 'modal' => true, 49 | 'attachmentSupport' => true, 50 | 'noCSS' => true, # Loading the CSS from the plugin 51 | ) 52 | ); 53 | 54 | // Localize Script 55 | wp_add_inline_script('zammad_wp_form', 'window.formOptions =' . json_encode(wp_parse_args($args, $defaults), JSON_UNESCAPED_UNICODE), 'before'); 56 | 57 | // Enqueue styles 58 | wp_enqueue_style('zammad_wp_form'); 59 | 60 | // Zammad Form Script requires id "zammad_form_script" 61 | add_filter( 62 | 'script_loader_tag', 63 | function ($tag, $handle, $source) { 64 | if ($handle === 'zammad_form') { 65 | $tag = ''; 66 | } 67 | return $tag; 68 | }, 69 | 10, 70 | 3 71 | ); 72 | 73 | // Enqueue scripts 74 | wp_enqueue_script('zammad_form'); 75 | wp_enqueue_script('zammad_wp_form'); 76 | } 77 | -------------------------------------------------------------------------------- /includes/functions/hf-plugin-integration.php: -------------------------------------------------------------------------------- 1 | group()->allGroups(); 34 | $ticket_priority = $zammad->ticket()->allTicketPriorities(); 35 | $ticket_state = $zammad->ticket()->allTicketStates(); 36 | 37 | ?> 38 | %s: Create ticket with state %s for Group %s with priority %s.', $zammad->url, $settings['state'], $settings['group'], $settings['priority']); ?> 39 | 40 | 41 | 42 | 43 | 44 | 52 | 53 | 54 | 55 | 63 | 64 | 65 | 66 | 74 | 75 | 76 | 77 | 81 | 82 | 83 | 84 | 92 | 93 | 94 | 95 | 99 | 100 |
45 | 51 |
56 | 62 |
67 | 73 |
78 | 79 |

80 |
85 | 86 |

87 | 88 | 89 |
90 |

91 |
96 | 97 |

98 |
101 | data, CASE_UPPER); 135 | 136 | // Find email field 137 | $email_address = ''; 138 | 139 | // Presort essential fields 140 | foreach ($data as $field => $value) { 141 | if (is_string($value) && is_email($value)) { 142 | $email_address = $value; 143 | } elseif (hf_is_file($value)) { 144 | // todo: Currently no support for multiple files 145 | // @see: https://github.com/ibericode/html-forms/issues/53 146 | $files[] = $value; 147 | } 148 | } 149 | 150 | // Bail if no email address found 151 | if (empty($email_address)) { 152 | return false; 153 | } 154 | 155 | // Handle Name Fields 156 | $firstname = hf_array_get($data, 'FIRSTNAME', ''); 157 | $lastname = hf_array_get($data, 'LASTNAME', ''); 158 | 159 | if (empty($fistname) && empty($lastname)) { 160 | $name = hf_array_get($data, 'NAME', ''); 161 | 162 | if( $names = explode(' ', $name, 2) ) { 163 | if (count($names) === 2) { 164 | list( $firstname, $lastname ) = $names; 165 | } else { 166 | $lastname = $name; 167 | } 168 | } 169 | } 170 | 171 | // Search for existing user in Zammad 172 | // @see: https://docs.zammad.org/en/latest/api/user.html 173 | $users = $zammad->user()->searchUsers($email_address, 1, 1); 174 | 175 | // @see: https://docs.zammad.org/en/latest/api/user.html#create 176 | $user_meta = apply_filters('zammad_wp:hf_action:user_fields', (array) [ 177 | 'firstname' => $firstname, 178 | 'lastname' => $lastname, 179 | 'preferences' => [ 180 | 'locale' => get_locale() 181 | ] 182 | ], $submission); 183 | 184 | if ($users) { 185 | // Get first user result & update data 186 | $user = $users[0]->setValues($user_meta); 187 | // Get user id 188 | $user_id = $users[0]->getValue('id'); 189 | // Save additional data 190 | $user->save(); 191 | } else { 192 | // No user found, create a new one 193 | $user_id = $zammad->user()->createUser(array_merge([ 194 | 'login' => $email_address, 195 | 'email' => $email_address, 196 | ], $user_meta))->getValue('id'); 197 | } 198 | 199 | // Set on behalf 200 | $customer = new ZammadWp\Zammad([ 201 | 'on_behalf_user' => $user_id 202 | ]); 203 | 204 | // Prepare ticket values 205 | // @see: https://docs.zammad.org/en/latest/api/ticket.html#create 206 | $subject = apply_filters( 207 | 'zammad_wp:hf_action:subject', 208 | hf_replace_data_variables($settings['subject'], $data, 'strip_tags'), 209 | $submission 210 | ); 211 | $message = apply_filters( 212 | 'zammad_wp:hf_action:message', 213 | hf_replace_data_variables($settings['message'], $data, 'esc_html'), 214 | $submission 215 | ); 216 | $attachments = apply_filters( 217 | 'zammad_wp:hf_action:attachments', 218 | zammad_hf_prepare_attachments($files), 219 | $submission 220 | ); 221 | $tags = apply_filters( 222 | 'zammad_wp:hf_action:tags', 223 | hf_replace_data_variables($settings['tags'], $data, 'strip_tags'), 224 | $submission 225 | ); 226 | 227 | // Prepare Ticket 228 | $ticket = apply_filters('zammad_wp:hf_action:ticket_fields', [ 229 | 'title' => $subject, 230 | 'customer_id' => $user_id, 231 | 'group' => (string) $settings['group'], 232 | 'priority' => (string) $settings['priority'], 233 | 'state' => (string) $settings['state'], 234 | 'tags' => $tags 235 | ], $submission); 236 | 237 | // Prepare Article 238 | // @see: https://community.zammad.org/t/custom-form-to-create-new-ticket-via-api/2068/2 239 | $ticket['article'] = apply_filters('zammad_wp:hf_action:article_fields', [ 240 | 'sender' => 'Customer', 241 | 'subject' => $subject, 242 | 'body' => $message, 243 | 'content_type' => 'text/html', 244 | 'type' => 'web', 245 | 'internal' => false, 246 | 'attachments' => $attachments 247 | ], $submission); 248 | 249 | // Create the ticket 250 | $ticket = $customer->ticket()->createTicket($ticket); 251 | 252 | // Ticket creation success/error 253 | if (is_int($ticket)) { 254 | return $ticket; 255 | } else { 256 | return false; 257 | } 258 | } 259 | } 260 | 261 | /** 262 | * Prepare attached files for Zammad API 263 | * @see: https://docs.zammad.org/en/latest/api/ticket/articles.html#create 264 | * 265 | * @param array $files 266 | * @return array 267 | */ 268 | function zammad_hf_prepare_attachments($files) 269 | { 270 | $attachments = []; 271 | 272 | foreach ( $files as $file ) { 273 | // Supports HTML Forms ($file['tmp_name']) & HTML Forms Premium ($file['name']) 274 | if ((isset($file['dir']) || isset($file['tmp_name'])) && isset($file['name']) ) { 275 | $content = file_get_contents($file['tmp_name'] ?? trailingslashit($file['dir']) . $file['name']); 276 | $attachments[] = [ 277 | 'filename' => $file['name'], 278 | 'data' => base64_encode( $content ), 279 | 'mime-type' => isset( $file['attachment_id'] ) ? get_post_mime_type( $file['attachment_id'] ) : $file['type'] 280 | ]; 281 | } 282 | } 283 | 284 | return $attachments; 285 | } 286 | 287 | /** 288 | * Default Settings for HTML Forms Action 289 | * 290 | * @return array 291 | */ 292 | function zammad_hf_default_settings() 293 | { 294 | return array( 295 | // Ticket defaults 296 | 'title' => '[SUBJECT]', 297 | 'group' => null, 298 | 'state' => null, 299 | 'priority' => null, 300 | 301 | // Article defaults 302 | 'subject' => '[SUBJECT]', 303 | 'message' => '[MESSAGE]', 304 | 305 | // Meta defaults 306 | 'tags' => '', 307 | ); 308 | } 309 | -------------------------------------------------------------------------------- /languages/zammad-wp.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2020 Philipp Wellmer 2 | # This file is distributed under the same license as the Zammad for WordPress plugin. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Zammad for WordPress\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/zammad-wp\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: LANGUAGE \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "POT-Creation-Date: 2020-08-22T16:19:31+02:00\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "X-Generator: WP-CLI 2.4.0\n" 15 | 16 | #. Plugin Name of the plugin 17 | msgid "Zammad for WordPress" 18 | msgstr "" 19 | 20 | #. Plugin URI of the plugin 21 | msgid "https://ouun.io" 22 | msgstr "" 23 | 24 | #. Description of the plugin 25 | msgid "Integrate Zammad into WordPress" 26 | msgstr "" 27 | 28 | #. Author of the plugin 29 | msgid "Philipp Wellmer " 30 | msgstr "" 31 | 32 | #: includes/functions/chat.php:75 33 | msgid "Please send us your request and we will answer as soon as possible" 34 | msgstr "" 35 | 36 | #: includes/functions/chat.php:76 37 | msgid "Sorry for the delay, still connecting." 38 | msgstr "" 39 | 40 | #. translators: Placeholder is "click here" link text 41 | #: includes/functions/chat.php:79 42 | msgid "We do not want to let you wait anytime longer. Please feel free to use the form instead or %s to try again." 43 | msgstr "" 44 | 45 | #: includes/functions/chat.php:83 46 | msgid "click here" 47 | msgstr "" 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zammad-wp", 3 | "version": "1.0.0", 4 | "description": "Zammad integration for WordPress", 5 | "author": { 6 | "name": "Philipp Wellmer", 7 | "email": "philipp@ouun.io", 8 | "url": "https://ouun.io", 9 | "role": "developer" 10 | }, 11 | "scripts": { 12 | "test": "phpunit", 13 | "start": "composer install && npm install && npm run build", 14 | "build": "NODE_ENV=production webpack --config config/webpack.config.prod.js", 15 | "dev": "NODE_ENV=development webpack --config config/webpack.config.dev.js", 16 | "watch": "NODE_ENV=development webpack --watch --config config/webpack.config.dev.js", 17 | "build-release": "npm install && composer install --no-dev -o && npm run build", 18 | "lint-release": "npm install && composer install && npm run lint", 19 | "lint-css": "stylelint assets/css", 20 | "lint-js": "eslint assets/js", 21 | "lint-php": "composer run lint", 22 | "format-js": "eslint --fix assets/js", 23 | "lint": "npm run lint-css && npm run lint-js && npm run lint-php", 24 | "format": "npm run format-js" 25 | }, 26 | "husky": { 27 | "hooks": {} 28 | }, 29 | "lint-staged": { 30 | "*.css": [ 31 | "stylelint" 32 | ], 33 | "*.js": [ 34 | "eslint" 35 | ] 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/10up/plugin-scaffold" 40 | }, 41 | "devDependencies": { 42 | "@10up/babel-preset-default": "^1.0.3", 43 | "@10up/eslint-config": "^2.0.0", 44 | "@10up/stylelint-config": "^1.0.9", 45 | "@babel/core": "^7.10.5", 46 | "@babel/register": "^7.10.5", 47 | "@wordpress/eslint-plugin": "^4.1.0", 48 | "babel-eslint": "^10.1.0", 49 | "babel-loader": "^8.1.0", 50 | "browser-sync": "^2.26.10", 51 | "browser-sync-webpack-plugin": "^2.2.2", 52 | "browserslist": "^4.16.5", 53 | "caniuse-db": "^1.0.30001105", 54 | "clean-webpack-plugin": "^3.0.0", 55 | "copy-webpack-plugin": "^5.1.1", 56 | "core-js": "^3.6.5", 57 | "css-loader": "^3.6.0", 58 | "cssnano": "^4.1.10", 59 | "eslint": "^6.8.0", 60 | "eslint-config-airbnb": "^18.2.0", 61 | "eslint-config-airbnb-base": "^14.2.0", 62 | "eslint-config-prettier": "^6.11.0", 63 | "eslint-loader": "^3.0.4", 64 | "eslint-plugin-import": "^2.22.0", 65 | "eslint-plugin-jsdoc": "^22.2.0", 66 | "eslint-plugin-jsx-a11y": "^6.3.1", 67 | "eslint-plugin-prettier": "^3.1.4", 68 | "eslint-plugin-react": "^7.20.3", 69 | "eslint-plugin-react-hooks": "^2.5.1", 70 | "husky": "^3.1.0", 71 | "imagemin-webpack-plugin": "^2.4.2", 72 | "lint-staged": "^9.5.0", 73 | "mini-css-extract-plugin": "^0.9.0", 74 | "postcss-import": "^12.0.1", 75 | "postcss-loader": "^3.0.0", 76 | "postcss-preset-env": "^6.7.0", 77 | "prettier": "^2.0.5", 78 | "sass": "^1.26.10", 79 | "sass-loader": "^9.0.2", 80 | "stylelint": "^9.10.1", 81 | "stylelint-config-wordpress": "^14.0.0", 82 | "stylelint-declaration-use-variable": "^1.7.2", 83 | "stylelint-order": "^2.2.1", 84 | "stylelint-webpack-plugin": "^1.2.3", 85 | "terser": "^4.8.0", 86 | "webpack": "^4.44.0", 87 | "webpack-cli": "^3.3.12", 88 | "webpack-fix-style-only-entries": "^0.4.0", 89 | "webpack-merge": "^4.2.2", 90 | "webpackbar": "^4.0.0" 91 | }, 92 | "engines": { 93 | "node": ">=12.0.0" 94 | }, 95 | "dependencies": { 96 | "normalize.css": "^8.0.1" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A custom set for Zammad WPt 5 | 6 | 7 | /dist/* 8 | 9 | 10 | /tests/* 11 | 12 | 13 | /vendor/* 14 | 15 | 16 | /node_modules/* 17 | 18 | 19 | *.js 20 | *.css 21 | 22 | 23 | 24 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | ./tests/phpunit 9 | 10 | 11 | 12 | 13 | ./includes 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | 9 | * License: GPL v2 or later 10 | * License URI: https://www.gnu.org/licenses/gpl-2.0.html 11 | * GitHub Plugin URI: ouun/zammad-wp 12 | * Requires PHP: 7.2 13 | * Text Domain: zammad-wp 14 | * Domain Path: /languages 15 | * 16 | * @package ZammadWp 17 | */ 18 | 19 | // Useful global constants. 20 | define('ZAMMAD_WP_VERSION', '0.9.1'); 21 | define('ZAMMAD_WP_URL', plugin_dir_url(__FILE__)); 22 | define('ZAMMAD_WP_PATH', plugin_dir_path(__FILE__)); 23 | define('ZAMMAD_WP_INC', ZAMMAD_WP_PATH . 'includes/'); 24 | 25 | // Include files. 26 | require_once ZAMMAD_WP_INC . 'functions/core.php'; 27 | require_once ZAMMAD_WP_INC . 'functions/chat.php'; 28 | require_once ZAMMAD_WP_INC . 'functions/form.php'; 29 | require_once ZAMMAD_WP_INC . 'functions/hf-plugin-integration.php'; 30 | 31 | // Activation/Deactivation. 32 | register_activation_hook(__FILE__, '\ZammadWp\Core\activate'); 33 | register_deactivation_hook(__FILE__, '\ZammadWp\Core\deactivate'); 34 | 35 | // Bootstrap. 36 | ZammadWp\Core\setup(); 37 | 38 | // Require Composer autoloader if it exists. 39 | if (file_exists(ZAMMAD_WP_PATH . '/vendor/autoload.php')) { 40 | require_once ZAMMAD_WP_PATH . 'vendor/autoload.php'; 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Exports the PostCSS configuration. 3 | * 4 | * @return {string} PostCSS options. 5 | */ 6 | module.exports = ( { file, options, env } ) => ( { /* eslint-disable-line */ 7 | plugins: { 8 | 'postcss-import': {}, 9 | 'postcss-preset-env': { 10 | stage: 0, 11 | autoprefixer: { 12 | grid: true, 13 | }, 14 | }, 15 | // Minify style on production using cssano. 16 | cssnano: 17 | env === 'production' 18 | ? { 19 | preset: [ 20 | 'default', 21 | { 22 | autoprefixer: false, 23 | calc: { 24 | precision: 8, 25 | }, 26 | convertValues: true, 27 | discardComments: { 28 | removeAll: true, 29 | }, 30 | mergeLonghand: false, 31 | zindex: false, 32 | }, 33 | ], 34 | } 35 | : false, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Zammad for WordPress === 2 | Contributors: werepack, philippmuenchen 3 | Donate link: https://www.paypal.me/ouun 4 | Tags: zammad, live-chat, ticketing, forms, feedback 5 | Requires at least: 4.5 6 | Requires PHP: 7.2 7 | Tested up to: 6.1 8 | Stable tag: 0.9.1 9 | 10 | This plugin helps you embed Zammad Chats & Forms into your WordPress site and gives you access to the Zammad API if required. 11 | 12 | == Description == 13 | 14 | Please see the [GitHub Repository](https://github.com/ouun/zammad-wp) for a complete documentation. 15 | 16 | == Installation == 17 | 18 | = Composer Package (recommended) = 19 | 20 | Use `composer require ouun/zammad-wp` to install plugin and dependencies. 21 | 22 | = Manual Installation = 23 | 24 | Download latest version and install it as a regular WordPress Plugin from: 25 | https://github.com/ouun/zammad-wp/releases/latest/download/zammad-wp.zip 26 | 27 | 1. Upload the entire `/zammad-wp` directory to the `/wp-content/plugins/` directory. 28 | 2. Activate Zammad for WordPress through the 'Plugins' menu in WordPress. 29 | 3. Follow instructions in the [Wiki](https://github.com/ouun/zammad-wp/wiki). 30 | 31 | == Changelog == 32 | 33 | = 0.9.1 = 34 | * Adds PHP 8.x compatibility (#38) 35 | * Files Handling: Adds Support for HTML Forms Plugin (w/o Premium Extension) 36 | 37 | = 0.9.0 = 38 | * Enh: Forms now attach uploaded files to Zammad tickets 39 | * Enh: Update Dependencies 40 | * Fix: Forms: Fix handling if singe name field only contains single word 41 | 42 | = 0.8.3 = 43 | * Enh: Adds(test) support for GitHub Updater: https://github.com/afragen/github-updater 44 | * Fix: Vendor folder missing from compiled download ZIP 45 | 46 | = 0.8.2 = 47 | * Various fixes 48 | 49 | = 0.8.1 = 50 | * Various fixes 51 | * Move documentation to Wiki: https://github.com/ouun/zammad-wp/wiki 52 | * Exit Pre-Release state on GitHub 53 | 54 | = 0.8.0 = 55 | * Adds HTML Forms Plugin integration 56 | * Adds support for complex forms with custom fields 57 | * Adds custom html/form as chat fallback 58 | * Extends, fixes & improves ZammadWP API-Wrapper 59 | * Various smaller fixes 60 | 61 | = 0.7.0 = 62 | * Fix mobile button alignment 63 | * Various small bug fixes 64 | 65 | = 0.6.0 = 66 | * Minimize chat window while an active chat when clicking X 67 | * Prevent default behavior to close the connection while chatting by accident 68 | 69 | = 0.5.0 = 70 | * Adds more options to set custom messages 71 | 72 | = 0.4.0 = 73 | * Various fixes 74 | 75 | = 0.3.0 = 76 | * Fixes Chat Fallback to Form 77 | * Fixes missing form in backend 78 | 79 | = 0.2.0 = 80 | * Adds missing dist folder 81 | 82 | = 0.1.0 = 83 | * First release 84 | 85 | == Upgrade Notice == 86 | 87 | = 0.1.0 = 88 | First Release 89 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Zammad for Wordpress Scaffold 3 | */ 4 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Zammad for WordPress Scaffold 3 | */ 4 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertConditionsMet(); 38 | } 39 | 40 | /** 41 | * Test internationalization integration. 42 | */ 43 | public function test_i18n() { 44 | // Setup 45 | \WP_Mock::userFunction( 'get_locale', array( 46 | 'times' => 1, 47 | 'args' => array(), 48 | 'return' => 'en_US', 49 | ) ); 50 | \WP_Mock::onFilter( 'plugin_locale' )->with( 'en_US', 'zammad-wp' )->reply( 'en_US' ); 51 | \WP_Mock::userFunction( 'load_textdomain', array( 52 | 'times' => 1, 53 | 'args' => array( 'zammad-wp', 'lang_dir/zammad-wp/zammad-wp-en_US.mo' ), 54 | ) ); 55 | \WP_Mock::userFunction( 'plugin_basename', array( 56 | 'times' => 1, 57 | 'args' => array( 'path' ), 58 | 'return' => 'path', 59 | ) ); 60 | \WP_Mock::userFunction( 'load_plugin_textdomain', array( 61 | 'times' => 1, 62 | 'args' => array( 'zammad-wp', false, 'path/languages/' ), 63 | ) ); 64 | 65 | // Act 66 | i18n(); 67 | 68 | // Verify 69 | $this->assertConditionsMet(); 70 | } 71 | 72 | /** 73 | * Test initialization method. 74 | */ 75 | public function test_init() { 76 | // Setup 77 | \WP_Mock::expectAction( 'zammad_wp_init' ); 78 | 79 | // Act 80 | init(); 81 | 82 | // Verify 83 | $this->assertConditionsMet(); 84 | } 85 | 86 | /** 87 | * Test activation routine. 88 | */ 89 | public function test_activate() { 90 | // Setup 91 | \WP_Mock::userFunction( 'flush_rewrite_rules', array( 92 | 'times' => 1 93 | ) ); 94 | 95 | // Act 96 | activate(); 97 | 98 | // Verify 99 | $this->assertConditionsMet(); 100 | } 101 | 102 | /** 103 | * Test deactivation routine. 104 | */ 105 | public function test_deactivate() { 106 | // Setup 107 | 108 | // Act 109 | deactivate(); 110 | 111 | // Verify 112 | $this->assertTrue( true ); // Replace with actual assertion 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/phpunit/test-tools/TestCase.php: -------------------------------------------------------------------------------- 1 | setPreserveGlobalState( false ); 13 | return parent::run( $result ); 14 | } 15 | 16 | protected $testFiles = array(); 17 | 18 | public function setUp() :void { 19 | if ( ! empty( $this->testFiles ) ) { 20 | foreach ( $this->testFiles as $file ) { 21 | if ( file_exists( PROJECT . $file ) ) { 22 | require_once( PROJECT . $file ); 23 | } 24 | } 25 | } 26 | 27 | parent::setUp(); 28 | } 29 | 30 | public function assertActionsCalled() { 31 | $actions_not_added = $expected_actions = 0; 32 | try { 33 | WP_Mock::assertActionsCalled(); 34 | } catch ( Exception $e ) { 35 | $actions_not_added = 1; 36 | $expected_actions = $e->getMessage(); 37 | } 38 | $this->assertEmpty( $actions_not_added, $expected_actions ); 39 | } 40 | 41 | public function ns( $function ) { 42 | if ( ! is_string( $function ) || false !== strpos( $function, '\\' ) ) { 43 | return $function; 44 | } 45 | 46 | $thisClassName = trim( get_class( $this ), '\\' ); 47 | 48 | if ( ! strpos( $thisClassName, '\\' ) ) { 49 | return $function; 50 | } 51 | 52 | // $thisNamespace is constructed by exploding the current class name on 53 | // namespace separators, running array_slice on that array starting at 0 54 | // and ending one element from the end (chops the class name off) and 55 | // imploding that using namespace separators as the glue. 56 | $thisNamespace = implode( '\\', array_slice( explode( '\\', $thisClassName ), 0, - 1 ) ); 57 | 58 | return "$thisNamespace\\$function"; 59 | } 60 | 61 | /** 62 | * Define constants after requires/includes 63 | * 64 | * See http://kpayne.me/2012/07/02/phpunit-process-isolation-and-constant-already-defined/ 65 | * for more details 66 | * 67 | * @param \Text_Template $template 68 | */ 69 | public function prepareTemplate( Text_Template $template ) { 70 | $template->setVar( [ 71 | 'globals' => '$GLOBALS[\'__PHPUNIT_BOOTSTRAP\'] = \'' . $GLOBALS['__PHPUNIT_BOOTSTRAP'] . '\';', 72 | ] ); 73 | parent::prepareTemplate( $template ); 74 | } 75 | } 76 | --------------------------------------------------------------------------------