beautify.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /* global $ $create moveFocus */// dom.js
  2. /* global CodeMirror */
  3. /* global createHotkeyInput helpPopup */// util.js
  4. /* global editor */
  5. /* global prefs */
  6. /* global t */// localization.js
  7. 'use strict';
  8. CodeMirror.commands.beautify = cm => {
  9. // using per-section mode when code editor or applies-to block is focused
  10. const isPerSection = cm.display.wrapper.parentElement.contains(document.activeElement);
  11. beautify(isPerSection ? [cm] : editor.getEditors(), false);
  12. };
  13. prefs.subscribe('editor.beautify.hotkey', (key, value) => {
  14. const {extraKeys} = CodeMirror.defaults;
  15. for (const [key, cmd] of Object.entries(extraKeys)) {
  16. if (cmd === 'beautify') {
  17. delete extraKeys[key];
  18. break;
  19. }
  20. }
  21. if (value) {
  22. extraKeys[value] = 'beautify';
  23. }
  24. }, {runNow: true});
  25. /**
  26. * @name beautify
  27. * @param {CodeMirror[]} scope
  28. * @param {boolean} [ui=true]
  29. */
  30. async function beautify(scope, ui = true) {
  31. await require(['/vendor-overwrites/beautify/beautify-css-mod']); /* global css_beautify */
  32. const tabs = prefs.get('editor.indentWithTabs');
  33. const options = Object.assign(prefs.defaults['editor.beautify'], prefs.get('editor.beautify'));
  34. options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
  35. options.indent_char = tabs ? '\t' : ' ';
  36. if (ui) {
  37. createBeautifyUI(scope, options);
  38. }
  39. for (const cm of scope) {
  40. setTimeout(beautifyEditor, 0, cm, options, ui);
  41. }
  42. }
  43. function beautifyEditor(cm, options, ui) {
  44. const pos = options.translate_positions =
  45. [].concat.apply([], cm.doc.sel.ranges.map(r =>
  46. [Object.assign({}, r.anchor), Object.assign({}, r.head)]));
  47. const text = cm.getValue();
  48. const newText = css_beautify(text, options);
  49. if (newText !== text) {
  50. if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
  51. // clear the list if last change wasn't a css-beautify
  52. cm.beautifyChange = {};
  53. }
  54. cm.setValue(newText);
  55. const selections = [];
  56. for (let i = 0; i < pos.length; i += 2) {
  57. selections.push({anchor: pos[i], head: pos[i + 1]});
  58. }
  59. const {scrollX, scrollY} = window;
  60. cm.setSelections(selections);
  61. window.scrollTo(scrollX, scrollY);
  62. cm.beautifyChange[cm.changeGeneration()] = true;
  63. if (ui) {
  64. $('#help-popup button[role="close"]').disabled = false;
  65. }
  66. }
  67. }
  68. function createBeautifyUI(scope, options) {
  69. helpPopup.show(t('styleBeautify'),
  70. $create([
  71. $create('.beautify-options', [
  72. $createOption('.selector1,', 'selector_separator_newline'),
  73. $createOption('.selector2', 'newline_before_open_brace'),
  74. $createOption('{', 'newline_after_open_brace'),
  75. $createOption('border: none;', 'newline_between_properties', true),
  76. $createOption('display: block;', 'newline_before_close_brace', true),
  77. $createOption('}', 'newline_between_rules'),
  78. $createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'),
  79. $createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'),
  80. ]),
  81. $create('p.beautify-hint', [
  82. $create('span', t('styleBeautifyHint') + '\u00A0'),
  83. createHotkeyInput('editor.beautify.hotkey', {
  84. buttons: false,
  85. onDone: () => moveFocus($('#help-popup'), 0),
  86. }),
  87. ]),
  88. $create('.buttons', [
  89. $create('button', {
  90. attributes: {role: 'close'},
  91. onclick: helpPopup.close,
  92. }, t('confirmClose')),
  93. $create('button', {
  94. attributes: {role: 'undo'},
  95. onclick() {
  96. let undoable = false;
  97. for (const cm of scope) {
  98. const data = cm.beautifyChange;
  99. if (!data || !data[cm.changeGeneration()]) continue;
  100. delete data[cm.changeGeneration()];
  101. const {scrollX, scrollY} = window;
  102. cm.undo();
  103. cm.scrollIntoView(cm.getCursor());
  104. window.scrollTo(scrollX, scrollY);
  105. undoable |= data[cm.changeGeneration()];
  106. }
  107. this.disabled = !undoable;
  108. },
  109. }, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
  110. ]),
  111. ]));
  112. $('#help-popup').className = 'wide';
  113. $('.beautify-options').onchange = ({target}) => {
  114. const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
  115. prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
  116. if (target.parentNode.hasAttribute('newline')) {
  117. target.parentNode.setAttribute('newline', value.toString());
  118. }
  119. beautify(scope, false);
  120. };
  121. function $createOption(label, optionName, indent) {
  122. const value = options[optionName];
  123. return (
  124. $create('div', {attributes: {newline: value}}, [
  125. $create('span', indent ? {attributes: {indent: ''}} : {}, label),
  126. $create('div.select-resizer', [
  127. $create('select', {dataset: {option: optionName}}, [
  128. $create('option', {selected: !value}, '\xA0'),
  129. $create('option', {selected: value}, '\\n'),
  130. ]),
  131. $create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
  132. $create('SVG:path', {
  133. 'fill-rule': 'evenodd',
  134. 'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
  135. '19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z',
  136. }),
  137. ]),
  138. ]),
  139. ])
  140. );
  141. }
  142. function $createLabeledCheckbox(optionName, i18nKey) {
  143. return (
  144. $create('label', {style: 'display: block; clear: both;'}, [
  145. $create('input', {
  146. type: 'checkbox',
  147. dataset: {option: optionName},
  148. checked: options[optionName] !== false,
  149. }),
  150. $create('SVG:svg.svg-icon.checked',
  151. $create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
  152. t(i18nKey),
  153. ])
  154. );
  155. }
  156. }
  157. /* exported initBeautifyButton */
  158. function initBeautifyButton(btn, scope) {
  159. btn.onclick = btn.oncontextmenu = e => {
  160. e.preventDefault();
  161. beautify(scope || editor.getEditors(), e.type === 'click');
  162. };
  163. }