Mocoapp Textarea Fix

Allows to add line breaks in the description textarea of the Moco booking tool

  1. // ==UserScript==
  2. // @name Mocoapp Textarea Fix
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-04-08-03
  5. // @description Allows to add line breaks in the description textarea of the Moco booking tool
  6. // @author opctim
  7. // @license MIT
  8. // @match https://*.mocoapp.com/*
  9. // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI3LjguMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxvZ29fUkdCIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0ZGRkZGRjt9Cgkuc3Qxe2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzM4QjVFQjt9Cgkuc3Qye2ZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO30KCS5zdDN7ZmlsbDpub25lO30KPC9zdHlsZT4KPGc+Cgk8Zz4KCQk8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSIxNiIgY3k9IjE2IiByPSIxMS44Ii8+CgkJPGc+CgkJCTxnPgoJCQkJPHBhdGggY2xhc3M9InN0MSIgZD0iTTE2LjEsOS45Yy0yLDAtMy45LDAuOC01LjMsMi4xYzAuMy0wLjEsMC42LTAuMSwwLjktMC4xYzEuNiwwLDMsMS4zLDMsM2MwLDEuNi0xLjMsMy0zLDMKCQkJCQljLTEuNSwwLTIuNy0xLjEtMi45LTIuNmMtMC4yLDAuNy0wLjQsMS41LTAuNCwyLjNjMCw0LjIsMy40LDcuNyw3LjcsNy43YzQuMiwwLDcuNy0zLjQsNy43LTcuN0MyMy44LDEzLjQsMjAuNCw5LjksMTYuMSw5Ljl6IgoJCQkJCS8+CgkJCQk8Zz4KCQkJCQk8Zz4KCQkJCQkJPHBhdGggZD0iTTE2LDMxLjljLTIuMiwwLTQuMy0wLjQtNi4yLTEuM2MtMS45LTAuOC0zLjYtMi01LTMuNGMtMS40LTEuNC0yLjYtMy4xLTMuNC01Yy0wLjgtMS45LTEuMy00LTEuMy02LjIKCQkJCQkJCWMwLTIuMiwwLjQtNC4zLDEuMy02LjJjMC44LTEuOSwyLTMuNiwzLjQtNWMxLjQtMS40LDMuMS0yLjYsNS0zLjRjMS45LTAuOCw0LTEuMyw2LjItMS4zYzIuMiwwLDQuMywwLjQsNi4yLDEuMwoJCQkJCQkJYzEuOSwwLjgsMy42LDIsNSwzLjRjMS40LDEuNCwyLjYsMy4xLDMuNCw1YzAuOCwxLjksMS4zLDQsMS4zLDYuMmMwLDIuMi0wLjQsNC4zLTEuMyw2LjJjLTAuOCwxLjktMiwzLjYtMy40LDUKCQkJCQkJCWMtMS40LDEuNC0zLjEsMi42LTUsMy40QzIwLjMsMzEuNSwxOC4yLDMxLjksMTYsMzEuOXogTTE2LDQuNmMtMS42LDAtMy4xLDAuMy00LjUsMC45Yy0xLjQsMC42LTIuNiwxLjQtMy42LDIuNAoJCQkJCQkJYy0xLDEtMS45LDIuMi0yLjUsMy42QzQuOCwxMyw0LjUsMTQuNCw0LjUsMTZjMCwxLjYsMC4zLDMuMSwwLjksNC41YzAuNiwxLjQsMS40LDIuNiwyLjUsMy42YzEsMSwyLjMsMS45LDMuNiwyLjQKCQkJCQkJCWMxLjQsMC42LDIuOSwwLjksNC41LDAuOWMxLjYsMCwzLTAuMyw0LjQtMC45YzEuNC0wLjYsMi42LTEuNCwzLjYtMi40YzEtMSwxLjgtMi4zLDIuNC0zLjZjMC42LTEuNCwwLjktMi45LDAuOS00LjUKCQkJCQkJCWMwLTEuNi0wLjMtMy0wLjktNC40Yy0wLjYtMS40LTEuNC0yLjYtMi40LTMuNmMtMS0xLTIuMi0xLjgtMy42LTIuNEMxOSw0LjksMTcuNiw0LjYsMTYsNC42eiIvPgoJCQkJCTwvZz4KCQkJCTwvZz4KCQkJCTxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xNi4xLDEyLjljLTAuNywwLTEuMywwLjEtMS45LDAuNGMwLjMsMC41LDAuNSwxLDAuNSwxLjZjMCwxLjYtMS4zLDMtMywzYy0wLjEsMC0wLjIsMC0wLjMsMAoJCQkJCWMwLjEsMi41LDIuMiw0LjUsNC43LDQuNWMyLjYsMCw0LjctMi4xLDQuNy00LjdTMTguNywxMi45LDE2LjEsMTIuOXoiLz4KCQkJPC9nPgoJCTwvZz4KCTwvZz4KCTxyZWN0IGNsYXNzPSJzdDMiIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIvPgo8L2c+Cjwvc3ZnPgo=
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. window.addEventListener('load', function () {
  17. const style = document.createElement('style');
  18. style.innerHTML = '.activity-row .third > .flex > div, .tst-timesheet-activity span[title] { white-space: pre-wrap }';
  19. document.head.appendChild(style);
  20.  
  21. const attachListener = (textarea) => {
  22. // Avoid attaching the listener multiple times
  23. if (textarea._shiftEnterHandled) return;
  24. textarea._shiftEnterHandled = true;
  25.  
  26. textarea.addEventListener('keydown', function (event) {
  27. if (event.key === 'Enter' && event.shiftKey) {
  28. event.stopPropagation();
  29. console.log('Shift + Enter stopped on:', textarea);
  30. }
  31. });
  32. };
  33.  
  34. // Attach to existing elements
  35. document.querySelectorAll('textarea[name="description"]').forEach(attachListener);
  36.  
  37. // Observe DOM changes to catch newly added textareas
  38. const observer = new MutationObserver((mutations) => {
  39. for (const mutation of mutations) {
  40. for (const node of mutation.addedNodes) {
  41. if (node.nodeType !== Node.ELEMENT_NODE) continue;
  42.  
  43. if (node.matches?.('textarea[name="description"]')) {
  44. attachListener(node);
  45. }
  46.  
  47. node.querySelectorAll?.('textarea[name="description"]').forEach(attachListener);
  48. }
  49. }
  50. });
  51.  
  52. observer.observe(document.body, {
  53. childList: true,
  54. subtree: true,
  55. });
  56. });
  57. })();