editor.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import $ from 'jquery';
  2. import Buttons, { strategies as buttonStrategies } from './editor/buttons';
  3. import codemirror from 'codemirror';
  4. import { watch } from 'watchjs';
  5. import jsyaml from 'js-yaml';
  6. global.jsyaml = jsyaml;
  7. // Modes
  8. import 'codemirror/mode/css/css';
  9. import 'codemirror/mode/gfm/gfm';
  10. import 'codemirror/mode/htmlmixed/htmlmixed';
  11. import 'codemirror/mode/javascript/javascript';
  12. import 'codemirror/mode/markdown/markdown';
  13. import 'codemirror/mode/php/php';
  14. import 'codemirror/mode/sass/sass';
  15. import 'codemirror/mode/twig/twig';
  16. import 'codemirror/mode/xml/xml';
  17. import 'codemirror/mode/yaml/yaml';
  18. // Add-ons
  19. import 'codemirror/addon/edit/continuelist';
  20. import 'codemirror/addon/mode/overlay';
  21. import 'codemirror/addon/selection/active-line';
  22. import 'codemirror/addon/lint/lint';
  23. import 'codemirror/addon/lint/lint.css';
  24. import 'codemirror/addon/lint/css-lint';
  25. import 'codemirror/addon/lint/javascript-lint';
  26. import 'codemirror/addon/lint/json-lint';
  27. import 'codemirror/addon/lint/yaml-lint';
  28. let IS_MOUSEDOWN = false;
  29. const ThemesMap = ['paper'];
  30. const Defaults = {
  31. codemirror: {
  32. mode: 'htmlmixed',
  33. theme: 'paper',
  34. lineWrapping: true,
  35. dragDrop: true,
  36. autoCloseTags: true,
  37. matchTags: true,
  38. autoCloseBrackets: true,
  39. matchBrackets: true,
  40. indentUnit: 4,
  41. indentWithTabs: false,
  42. tabSize: 4,
  43. hintOptions: { completionSingle: false },
  44. extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
  45. }
  46. };
  47. export default class EditorField {
  48. constructor(options) {
  49. let body = $('body');
  50. this.editors = $();
  51. this.options = Object.assign({}, Defaults, options);
  52. this.buttons = Buttons;
  53. this.buttonStrategies = buttonStrategies;
  54. watch(Buttons, (/* key, modifier, prev, next */) => {
  55. this.editors.each((index, editor) => $(editor).data('toolbar').renderButtons());
  56. });
  57. $('[data-grav-editor]').each((index, editor) => this.addEditor(editor));
  58. $(() => { body.trigger('grav-editor-ready'); });
  59. body.on('mutation._grav', this._onAddedNodes.bind(this));
  60. body.on('mouseup._grav', () => {
  61. if (!IS_MOUSEDOWN) { return true; }
  62. body.unbind('mousemove._grav');
  63. IS_MOUSEDOWN = false;
  64. });
  65. body.on('mousedown._grav', '.grav-editor-resizer', (event) => {
  66. event && event.preventDefault();
  67. IS_MOUSEDOWN = true;
  68. let target = $(event.currentTarget);
  69. let container = target.siblings('.grav-editor-content');
  70. let editor = container.find('.CodeMirror');
  71. let codemirror = container.find('textarea').data('codemirror');
  72. body.on('mousemove._grav', (event) => {
  73. editor.css('height', Math.max(100, event.pageY - container.offset().top));
  74. codemirror.refresh();
  75. });
  76. });
  77. }
  78. addButton(button, options) {
  79. if (options && (options.before || options.after)) {
  80. let index = this.buttons.navigation.findIndex((obj) => {
  81. let key = Object.keys(obj).shift();
  82. return obj[key].identifier === (options.before || options.after);
  83. });
  84. if (!~index) {
  85. options = 'end';
  86. } else {
  87. this.buttons.navigation.splice(options.before ? index : index + 1, 0, button);
  88. }
  89. }
  90. if (options === 'start') { this.buttons.navigation.splice(0, 0, button); }
  91. if (!options || options === 'end') { this.buttons.navigation.push(button); }
  92. }
  93. addEditor(textarea) {
  94. textarea = $(textarea);
  95. let options = Object.assign(
  96. {},
  97. this.options.codemirror,
  98. textarea.data('grav-editor').codemirror
  99. );
  100. let theme = options.theme || 'paper';
  101. this.editors = this.editors.add(textarea);
  102. if (theme && !~ThemesMap.indexOf(theme)) {
  103. ThemesMap.push(theme);
  104. let themeCSS = `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.12.0/theme/${theme}.min.css`;
  105. $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', themeCSS));
  106. }
  107. if (options.mode === 'yaml') {
  108. Object.assign(options.extraKeys, { Tab: function(cm) { cm.replaceSelection(' ', 'end'); }});
  109. }
  110. let editor = codemirror.fromTextArea(textarea.get(0), options);
  111. textarea.data('codemirror', editor);
  112. textarea.data('toolbar', new Toolbar(textarea));
  113. textarea.addClass('code-mirrored');
  114. if (options.toolbar === false) {
  115. textarea.data('toolbar').ui.navigation.addClass('grav-editor-hide-toolbar');
  116. }
  117. editor.on('change', () => editor.save());
  118. }
  119. _onAddedNodes(event, target/* , record, instance */) {
  120. let editors = $(target).find('[data-grav-editor]');
  121. if (!editors.length) { return; }
  122. editors.each((index, editor) => {
  123. editor = $(editor);
  124. if (!~this.editors.index(editor)) {
  125. this.addEditor(editor);
  126. }
  127. });
  128. }
  129. }
  130. export class Toolbar {
  131. static templates() {
  132. return {
  133. navigation: `
  134. <div class="grav-editor-toolbar">
  135. <div class="grav-editor-actions"></div>
  136. <div class="grav-editor-modes"></div>
  137. </div>
  138. `
  139. };
  140. }
  141. constructor(editor) {
  142. this.editor = $(editor);
  143. this.codemirror = this.editor.data('codemirror');
  144. this.buttons = Buttons.navigation;
  145. this.ui = {
  146. navigation: $(Toolbar.templates().navigation)
  147. };
  148. this.editor.parent('.grav-editor-content')
  149. .before(this.ui.navigation)
  150. .after(this.ui.states);
  151. this.renderButtons();
  152. }
  153. renderButtons() {
  154. let map = { 'actions': 'navigation', 'modes': 'states'};
  155. ['actions', 'modes'].forEach((type) => {
  156. this.ui.navigation.find(`.grav-editor-${type}`).empty().append('<ul />');
  157. Buttons[map[type]].forEach((button) => this.renderButton(button, type));
  158. });
  159. }
  160. renderButton(button, type, location = null) {
  161. Object.keys(button).forEach((key) => {
  162. let obj = button[key];
  163. if (!obj.modes) { obj.modes = []; }
  164. if (!~this.codemirror.options.ignore.indexOf(key) && (!obj.modes.length || obj.modes.indexOf(this.codemirror.options.mode) > -1)) {
  165. let hint = obj.title ? `data-hint="${obj.title}"` : '';
  166. let element = $(`<li class="grav-editor-button-${key}"><a class="hint--top" ${hint}>${obj.label}</a></li>`);
  167. (location || this.ui.navigation.find(`.grav-editor-${type} ul:not(.dropdown-menu)`)).append(element);
  168. if (obj.shortcut) {
  169. this.addShortcut(obj.identifier, obj.shortcut, element);
  170. }
  171. obj.action && obj.action.call(obj.action, {
  172. codemirror: this.codemirror,
  173. button: element,
  174. textarea: this.editor,
  175. ui: this.ui
  176. });
  177. if (obj.children) {
  178. let childrenContainer = $('<ul class="dropdown-menu" />');
  179. element.addClass('button-group').find('a').wrap('<div class="dropdown-toggle" data-toggle="dropdown"></div>');
  180. element.find('a').append(' <i class="fa fa-caret-down"></i>');
  181. element.append(childrenContainer);
  182. obj.children.forEach((child) => this.renderButton(child, type, childrenContainer));
  183. }
  184. }
  185. });
  186. }
  187. addShortcut(identifier, shortcut, element) {
  188. let map = {};
  189. if (!Array.isArray(shortcut)) {
  190. shortcut = [shortcut];
  191. }
  192. shortcut.forEach((key) => {
  193. map[key] = () => {
  194. element.trigger(`click.editor.${identifier}`, [this.codemirror]);
  195. };
  196. });
  197. this.codemirror.addKeyMap(map);
  198. }
  199. }
  200. export let Instance = new EditorField();