util.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /* global $ $create getEventKeyName messageBoxProxy moveFocus */// dom.js
  2. /* global CodeMirror */
  3. /* global editor */
  4. /* global prefs */
  5. /* global t */// localization.js
  6. 'use strict';
  7. const helpPopup = {
  8. show(title = '', body) {
  9. const div = $('#help-popup');
  10. const contents = $('.contents', div);
  11. div.className = '';
  12. contents.textContent = '';
  13. if (body) {
  14. contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
  15. }
  16. $('.title', div).textContent = title;
  17. $('.dismiss', div).onclick = helpPopup.close;
  18. window.on('keydown', helpPopup.close, true);
  19. // reset any inline styles
  20. div.style = 'display: block';
  21. helpPopup.originalFocus = document.activeElement;
  22. return div;
  23. },
  24. close(event) {
  25. const canClose =
  26. !event ||
  27. event.type === 'click' || (
  28. getEventKeyName(event) === 'Escape' &&
  29. !$('.CodeMirror-hints, #message-box') && (
  30. !document.activeElement ||
  31. !document.activeElement.closest('#search-replace-dialog') &&
  32. document.activeElement.matches(':not(input), .can-close-on-esc')
  33. )
  34. );
  35. const div = $('#help-popup');
  36. if (!canClose || !div) {
  37. return;
  38. }
  39. if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
  40. setTimeout(async () => {
  41. const ok = await messageBoxProxy.confirm(t('confirmDiscardChanges'));
  42. return ok && helpPopup.close();
  43. });
  44. return;
  45. }
  46. if (div.contains(document.activeElement) && helpPopup.originalFocus) {
  47. helpPopup.originalFocus.focus();
  48. }
  49. const contents = $('.contents', div);
  50. div.style.display = '';
  51. contents.textContent = '';
  52. window.off('keydown', helpPopup.close, true);
  53. window.dispatchEvent(new Event('closeHelp'));
  54. },
  55. };
  56. // reroute handling to nearest editor when keypress resolves to one of these commands
  57. const rerouteHotkeys = {
  58. commands: [
  59. 'beautify',
  60. 'colorpicker',
  61. 'find',
  62. 'findNext',
  63. 'findPrev',
  64. 'jumpToLine',
  65. 'nextEditor',
  66. 'prevEditor',
  67. 'replace',
  68. 'replaceAll',
  69. 'save',
  70. 'toggleEditorFocus',
  71. 'toggleStyle',
  72. ],
  73. toggle(enable) {
  74. document[enable ? 'on' : 'off']('keydown', rerouteHotkeys.handler);
  75. },
  76. handler(event) {
  77. const keyName = CodeMirror.keyName(event);
  78. if (!keyName) {
  79. return;
  80. }
  81. const rerouteCommand = name => {
  82. if (rerouteHotkeys.commands.includes(name)) {
  83. CodeMirror.commands[name](editor.closestVisible(event.target));
  84. return true;
  85. }
  86. };
  87. if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
  88. CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
  89. event.preventDefault();
  90. event.stopPropagation();
  91. }
  92. },
  93. };
  94. function clipString(str, limit = 100) {
  95. return str.length <= limit ? str : str.substr(0, limit) + '...';
  96. }
  97. /* exported createHotkeyInput */
  98. function createHotkeyInput(prefId, {buttons = true, onDone}) {
  99. const RX_ERR = new RegExp('^(' + [
  100. /Space/,
  101. /(Shift-)?./, // a single character
  102. /(?=.)(Shift-?|Ctrl-?|Control-?|Alt-?|Meta-?)*(Escape|Tab|Page(Up|Down)|Arrow(Up|Down|Left|Right)|Home|End)?/,
  103. ].map(r => r.source || r).join('|') + ')$', 'i');
  104. const initialValue = prefs.get(prefId);
  105. const input = $create('input', {
  106. spellcheck: false,
  107. onpaste: e => onkeydown(e, e.clipboardData.getData('text')),
  108. onkeydown,
  109. });
  110. buttons = buttons && [
  111. ['confirmOK', 'Enter'],
  112. ['undo', initialValue],
  113. ['genericResetLabel', ''],
  114. ].map(([label, val]) =>
  115. $create('button', {onclick: e => onkeydown(e, val)}, t(label)));
  116. const [btnOk, btnUndo, btnReset] = buttons || [];
  117. onkeydown(null, initialValue);
  118. return buttons
  119. ? $create('fragment', [input, $create('.buttons', buttons)])
  120. : input;
  121. function onkeydown(e, key) {
  122. let newValue;
  123. if (e && e.type === 'keydown') {
  124. key = getEventKeyName(e);
  125. }
  126. switch (e && key) {
  127. case 'Tab':
  128. case 'Shift-Tab':
  129. return;
  130. case 'BackSpace':
  131. case 'Delete':
  132. newValue = '';
  133. break;
  134. case 'Enter':
  135. if (input.checkValidity() && onDone) onDone();
  136. break;
  137. case 'Escape':
  138. if (onDone) onDone();
  139. break;
  140. default:
  141. newValue = key.replace(/\b.$/, c => c.toUpperCase());
  142. }
  143. if (newValue != null) {
  144. const error = RX_ERR.test(newValue) ? t('genericError') : '';
  145. if (e && !error) prefs.set(prefId, newValue);
  146. input.setCustomValidity(error);
  147. input.value = newValue;
  148. input.focus();
  149. if (buttons) {
  150. btnOk.disabled = Boolean(error);
  151. btnUndo.disabled = newValue === initialValue;
  152. btnReset.disabled = !newValue;
  153. }
  154. }
  155. if (e) {
  156. e.preventDefault();
  157. e.stopPropagation();
  158. }
  159. }
  160. }
  161. /* exported showCodeMirrorPopup */
  162. function showCodeMirrorPopup(title, html, options) {
  163. const popup = helpPopup.show(title, html);
  164. popup.classList.add('big');
  165. let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
  166. mode: 'css',
  167. lineNumbers: true,
  168. lineWrapping: prefs.get('editor.lineWrapping'),
  169. foldGutter: true,
  170. gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
  171. matchBrackets: true,
  172. styleActiveLine: true,
  173. theme: prefs.get('editor.theme'),
  174. keyMap: prefs.get('editor.keyMap'),
  175. }, options));
  176. cm.focus();
  177. document.documentElement.style.pointerEvents = 'none';
  178. popup.style.pointerEvents = 'auto';
  179. const onKeyDown = event => {
  180. if (event.key === 'Tab' && !event.ctrlKey && !event.altKey && !event.metaKey) {
  181. const search = $('#search-replace-dialog');
  182. const area = search && search.contains(document.activeElement) ? search : popup;
  183. moveFocus(area, event.shiftKey ? -1 : 1);
  184. event.preventDefault();
  185. }
  186. };
  187. window.on('keydown', onKeyDown, true);
  188. window.on('closeHelp', () => {
  189. window.off('keydown', onKeyDown, true);
  190. document.documentElement.style.removeProperty('pointer-events');
  191. cm = popup.codebox = null;
  192. }, {once: true});
  193. return popup;
  194. }
  195. /* exported trimCommentLabel */
  196. function trimCommentLabel(str, limit = 1000) {
  197. // stripping /*** foo ***/ to foo
  198. return clipString(str.replace(/^[!-/:;=\s]*|[-#$&(+,./:;<=>\s*]*$/g, ''), limit);
  199. }