Просмотр исходного кода

Merge pull request #2547 from weseek/support/hotkeys

Support/hotkeys
Yuki Takei 5 лет назад
Родитель
Сommit
98d381d0fd

+ 1 - 0
resource/locales/en_US/translation.json

@@ -318,6 +318,7 @@
       "Edit Page": "Edit Page",
       "Create Page": "Create Page",
       "Show Contributors": "Show Contributors",
+      "MirrorMode": "Mirror Mode",
       "Konami Code": "Konami Code",
       "konami_code_url": "https://en.wikipedia.org/wiki/Konami_Code"
     },

+ 1 - 0
resource/locales/ja_JP/translation.json

@@ -319,6 +319,7 @@
       "Edit Page": "ページ編集",
       "Create Page": "ページ作成",
       "Show Contributors": "コントリビューター<br>を表示",
+      "MirrorMode": "ミラーモード",
       "Konami Code": "コナミコマンド",
       "konami_code_url": "https://ja.wikipedia.org/wiki/コナミコマンド"
     },

+ 3 - 3
src/client/js/base.jsx

@@ -6,8 +6,8 @@ import Xss from '@commons/service/xss';
 import GrowiNavbar from './components/Navbar/GrowiNavbar';
 import GrowiNavbarBottom from './components/Navbar/GrowiNavbarBottom';
 import Sidebar from './components/Sidebar';
+import HotkeysManager from './components/Hotkeys/HotkeysManager';
 import Fab from './components/Fab';
-import StaffCredit from './components/StaffCredit/StaffCredit';
 
 import AppContainer from './services/AppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
@@ -45,9 +45,9 @@ const componentMappings = {
 
   'grw-sidebar-wrapper': <Sidebar />,
 
-  'grw-fab-container': <Fab />,
+  'grw-hotkeys-manager': <HotkeysManager />,
 
-  'staff-credit': <StaffCredit />,
+  'grw-fab-container': <Fab />,
 };
 
 export { appContainer, componentMappings };

+ 80 - 0
src/client/js/components/Hotkeys/HotkeysDetector.jsx

@@ -0,0 +1,80 @@
+import React, { useMemo, useCallback } from 'react';
+import PropTypes from 'prop-types';
+
+import { GlobalHotKeys } from 'react-hotkeys';
+
+import HotkeyStroke from '../../models/HotkeyStroke';
+
+const HotkeysDetector = (props) => {
+
+  const { keySet, strokeSet, onDetected } = props;
+
+  // memorize HotkeyStroke instances
+  const hotkeyStrokes = useMemo(
+    () => {
+      const strokes = Array.from(strokeSet);
+      return strokes.map(stroke => new HotkeyStroke(stroke));
+    },
+    [strokeSet],
+  );
+
+  /**
+   * return key expression string includes modifier
+   */
+  const getKeyExpression = useCallback((event) => {
+    let eventKey = event.key;
+
+    if (event.ctrlKey) {
+      eventKey += '+ctrl';
+    }
+    if (event.metaKey) {
+      eventKey += '+meta';
+    }
+    if (event.altKey) {
+      eventKey += '+alt';
+    }
+    if (event.shiftKey) {
+      eventKey += '+shift';
+    }
+
+    return eventKey;
+  }, []);
+
+  /**
+   * evaluate the key user pressed and trigger onDetected
+   */
+  const checkHandler = useCallback((event) => {
+    event.preventDefault();
+
+    const eventKey = getKeyExpression(event);
+
+    hotkeyStrokes.forEach((hotkeyStroke) => {
+      if (hotkeyStroke.evaluate(eventKey)) {
+        onDetected(hotkeyStroke.stroke);
+      }
+    });
+  }, [hotkeyStrokes, getKeyExpression, onDetected]);
+
+  // memorize keyMap for GlobalHotKeys
+  const keyMap = useMemo(() => {
+    return { check: Array.from(keySet) };
+  }, [keySet]);
+
+  // memorize handlers for GlobalHotKeys
+  const handlers = useMemo(() => {
+    return { check: checkHandler };
+  }, [checkHandler]);
+
+  return (
+    <GlobalHotKeys keyMap={keyMap} handlers={handlers} />
+  );
+
+};
+
+HotkeysDetector.propTypes = {
+  onDetected: PropTypes.func.isRequired,
+  keySet: PropTypes.instanceOf(Set).isRequired,
+  strokeSet: PropTypes.instanceOf(Set).isRequired,
+};
+
+export default HotkeysDetector;

+ 79 - 0
src/client/js/components/Hotkeys/HotkeysManager.jsx

@@ -0,0 +1,79 @@
+import React, { useState } from 'react';
+
+import HotkeysDetector from './HotkeysDetector';
+
+import ShowStaffCredit from './Subscribers/ShowStaffCredit';
+import SwitchToMirrorMode from './Subscribers/SwitchToMirrorMode';
+import ShowShortcutsModal from './Subscribers/ShowShortcutsModal';
+import CreatePage from './Subscribers/CreatePage';
+import EditPage from './Subscribers/EditPage';
+
+// define supported components list
+const SUPPORTED_COMPONENTS = [
+  ShowStaffCredit,
+  SwitchToMirrorMode,
+  ShowShortcutsModal,
+  CreatePage,
+  EditPage,
+];
+
+const KEY_SET = new Set();
+const STROKE_SET = new Set();
+const STROKE_TO_COMPONENT_MAP = {};
+
+SUPPORTED_COMPONENTS.forEach((comp) => {
+  const strokes = comp.getHotkeyStrokes();
+
+  strokes.forEach((stroke) => {
+    // register key
+    stroke.forEach(key => KEY_SET.add(key));
+    // register stroke
+    STROKE_SET.add(stroke);
+    // register component
+    const componentList = STROKE_TO_COMPONENT_MAP[stroke] || [];
+    componentList.push(comp);
+    STROKE_TO_COMPONENT_MAP[stroke.toString()] = componentList;
+  });
+});
+
+const HotkeysManager = (props) => {
+  const [view, setView] = useState([]);
+
+  /**
+   * delete the instance in state.view
+   */
+  const deleteRender = (instance) => {
+    const index = view.lastIndexOf(instance);
+
+    const newView = view.slice(); // shallow copy
+    newView.splice(index, 1);
+    setView(newView);
+  };
+
+  /**
+   * activates when one of the hotkey strokes gets determined from HotkeysDetector
+   */
+  const onDetected = (strokeDetermined) => {
+    const key = (Math.random() * 1000).toString();
+    const components = STROKE_TO_COMPONENT_MAP[strokeDetermined.toString()];
+
+    const newViews = components.map(Component => (
+      <Component key={key} onDeleteRender={deleteRender} />
+    ));
+    setView(view.concat(newViews).flat());
+  };
+
+  return (
+    <>
+      <HotkeysDetector
+        onDetected={stroke => onDetected(stroke)}
+        keySet={KEY_SET}
+        strokeSet={STROKE_SET}
+      />
+      {view}
+    </>
+  );
+
+};
+
+export default HotkeysManager;

+ 31 - 0
src/client/js/components/Hotkeys/Subscribers/CreatePage.jsx

@@ -0,0 +1,31 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+import NavigationContainer from '../../../services/NavigationContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
+const CreatePage = (props) => {
+
+  // setup effect
+  useEffect(() => {
+    props.navigationContainer.openPageCreateModal();
+
+    // remove this
+    props.onDeleteRender(this);
+  }, [props]);
+
+  return <></>;
+};
+
+CreatePage.propTypes = {
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
+  onDeleteRender: PropTypes.func.isRequired,
+};
+
+const CreatePageWrapper = withUnstatedContainers(CreatePage, [NavigationContainer]);
+
+CreatePageWrapper.getHotkeyStrokes = () => {
+  return [['c']];
+};
+
+export default CreatePageWrapper;

+ 30 - 0
src/client/js/components/Hotkeys/Subscribers/EditPage.jsx

@@ -0,0 +1,30 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+const EditPage = (props) => {
+
+  // setup effect
+  useEffect(() => {
+    // ignore when dom that has 'modal in' classes exists
+    if (document.getElementsByClassName('modal in').length > 0) {
+      return;
+    }
+    // show editor
+    $('a[data-toggle="tab"][href="#edit"]').tab('show');
+
+    // remove this
+    props.onDeleteRender(this);
+  }, [props]);
+
+  return <></>;
+};
+
+EditPage.propTypes = {
+  onDeleteRender: PropTypes.func.isRequired,
+};
+
+EditPage.getHotkeyStrokes = () => {
+  return [['e']];
+};
+
+export default EditPage;

+ 26 - 0
src/client/js/components/Hotkeys/Subscribers/ShowShortcutsModal.jsx

@@ -0,0 +1,26 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+const ShowShortcutsModal = (props) => {
+
+  // setup effect
+  useEffect(() => {
+    // show modal to create a page
+    $('#shortcuts-modal').modal('toggle');
+
+    // remove this
+    props.onDeleteRender(this);
+  }, [props]);
+
+  return <></>;
+};
+
+ShowShortcutsModal.propTypes = {
+  onDeleteRender: PropTypes.func.isRequired,
+};
+
+ShowShortcutsModal.getHotkeyStrokes = () => {
+  return [['/+ctrl'], ['/+meta']];
+};
+
+export default ShowShortcutsModal;

+ 20 - 0
src/client/js/components/Hotkeys/Subscribers/ShowStaffCredit.jsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import StaffCredit from '../../StaffCredit/StaffCredit';
+
+const ShowStaffCredit = (props) => {
+
+  return <StaffCredit onClosed={() => props.onDeleteRender(this)} />;
+
+};
+
+ShowStaffCredit.propTypes = {
+  onDeleteRender: PropTypes.func.isRequired,
+};
+
+ShowStaffCredit.getHotkeyStrokes = () => {
+  return [['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']];
+};
+
+export default ShowStaffCredit;

+ 25 - 0
src/client/js/components/Hotkeys/Subscribers/SwitchToMirrorMode.jsx

@@ -0,0 +1,25 @@
+import React, { useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+const SwitchToMirrorMode = (props) => {
+
+  // setup effect
+  useEffect(() => {
+    document.body.classList.add('mirror');
+
+    // remove this
+    props.onDeleteRender(this);
+  }, [props]);
+
+  return <></>;
+};
+
+SwitchToMirrorMode.propTypes = {
+  onDeleteRender: PropTypes.func.isRequired,
+};
+
+SwitchToMirrorMode.getHotkeyStrokes = () => {
+  return [['x', 'x', 'b', 'b', 'a', 'y', 'a', 'y', 'ArrowDown', 'ArrowLeft']];
+};
+
+export default SwitchToMirrorMode;

+ 42 - 57
src/client/js/components/StaffCredit/StaffCredit.jsx

@@ -1,16 +1,11 @@
 import React from 'react';
-import { GlobalHotKeys } from 'react-hotkeys';
-
+import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import {
   Modal, ModalBody,
 } from 'reactstrap';
-
 import contributors from './Contributor';
 
-// px / sec
-const scrollSpeed = 200;
-
 /**
  * Page staff credit component
  *
@@ -19,56 +14,21 @@ const scrollSpeed = 200;
  * @extends {React.Component}
  */
 
+// eslint-disable-next-line no-unused-vars
+const logger = loggerFactory('growi:cli:StaffCredit');
+
 export default class StaffCredit extends React.Component {
 
   constructor(props) {
-    super(props);
-
-    this.logger = loggerFactory('growi:StaffCredit');
 
+    super(props);
     this.state = {
-      isShown: false,
-      userCommand: [],
+      isShown: true,
     };
-    this.konamiCommand = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
     this.deleteCredit = this.deleteCredit.bind(this);
   }
 
-  check(event) {
-    this.logger.debug(`'${event.key}' pressed`);
-
-    // compare keydown and next konamiCommand
-    if (this.konamiCommand[this.state.userCommand.length] === event.key) {
-      const nextValue = this.state.userCommand.concat(event.key);
-      if (nextValue.length === this.konamiCommand.length) {
-        this.setState({
-          isShown: true,
-          userCommand: [],
-        });
-        const target = $('.credit-curtain');
-        const scrollTargetHeight = target.children().innerHeight();
-        const duration = scrollTargetHeight / scrollSpeed * 1000;
-        target.animate({ scrollTop: scrollTargetHeight }, duration, 'linear');
-
-        target.slimScroll({
-          height: target.innerHeight(),
-          // Able to scroll after automatic schooling is complete so set "bottom" to allow scrolling from the bottom.
-          start: 'bottom',
-          color: '#FFFFFF',
-        });
-      }
-      else {
-        // add UserCommand
-        this.setState({ userCommand: nextValue });
-
-        this.logger.debug('userCommand', this.state.userCommand);
-      }
-    }
-    else {
-      this.setState({ userCommand: [] });
-    }
-  }
-
+  // to delete the staffCredit and to inform that to Hotkeys.jsx
   deleteCredit() {
     if (this.state.isShown) {
       this.setState({ isShown: false });
@@ -123,21 +83,46 @@ export default class StaffCredit extends React.Component {
     return null;
   }
 
+  componentDidMount() {
+    setTimeout(() => {
+      // px / sec
+      const scrollSpeed = 200;
+      const target = $('.credit-curtain');
+      const scrollTargetHeight = target.children().innerHeight();
+      const duration = scrollTargetHeight / scrollSpeed * 1000;
+      target.animate({ scrollTop: scrollTargetHeight }, duration, 'linear');
+      target.slimScroll({
+        height: target.innerHeight(),
+        // Able to scroll after automatic schooling is complete so set "bottom" to allow scrolling from the bottom.
+        start: 'bottom',
+        color: '#FFFFFF',
+      });
+    }, 10);
+  }
+
   render() {
-    const keyMap = { check: ['up', 'down', 'right', 'left', 'b', 'a'] };
-    const handlers = { check: (event) => { return this.check(event) } };
+    const { onClosed } = this.props;
+
     return (
-      <GlobalHotKeys keyMap={keyMap} handlers={handlers}>
-        <Modal isOpen={this.state.isShown} toggle={this.deleteCredit} scrollable className="staff-credit">
-          <ModalBody className="credit-curtain">
-            {this.renderContributors()}
-          </ModalBody>
-        </Modal>
-      </GlobalHotKeys>
+      <Modal
+        isOpen={this.state.isShown}
+        onClosed={() => {
+          if (onClosed != null) {
+            onClosed();
+          }
+        }}
+        toggle={this.deleteCredit}
+        scrollable
+        className="staff-credit"
+      >
+        <ModalBody className="credit-curtain">
+          {this.renderContributors()}
+        </ModalBody>
+      </Modal>
     );
   }
 
 }
-
 StaffCredit.propTypes = {
+  onClosed: PropTypes.func,
 };

+ 0 - 40
src/client/js/legacy/crowi.js

@@ -85,22 +85,6 @@ Crowi.modifyScrollTop = function() {
   }, timeout);
 };
 
-Crowi.handleKeyEHandler = (event) => {
-  // ignore when dom that has 'modal in' classes exists
-  if (document.getElementsByClassName('modal in').length > 0) {
-    return;
-  }
-  // show editor
-  $('a[data-toggle="tab"][href="#edit"]').tab('show');
-  event.preventDefault();
-};
-
-Crowi.handleKeyCtrlSlashHandler = (event) => {
-  // show modal to create a page
-  $('#shortcuts-modal').modal('toggle');
-  event.preventDefault();
-};
-
 Crowi.initClassesByOS = function() {
   // add classes to cmd-key by OS
   const platform = navigator.platform.toLowerCase();
@@ -410,30 +394,6 @@ window.addEventListener('hashchange', (e) => {
   }
 });
 
-window.addEventListener('keydown', (event) => {
-  const target = event.target;
-
-  // ignore when target dom is input
-  const inputPattern = /^input|textinput|textarea$/i;
-  if (inputPattern.test(target.tagName) || target.isContentEditable) {
-    return;
-  }
-
-  switch (event.key) {
-    case 'e':
-      if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
-        Crowi.handleKeyEHandler(event);
-      }
-      break;
-    case '/':
-      if (event.ctrlKey || event.metaKey) {
-        Crowi.handleKeyCtrlSlashHandler(event);
-      }
-      break;
-    default:
-  }
-});
-
 // adjust min-height of page for print temporarily
 window.onbeforeprint = function() {
   $('#page-wrapper').css('min-height', '0px');

+ 57 - 0
src/client/js/models/HotkeyStroke.js

@@ -0,0 +1,57 @@
+import loggerFactory from '@alias/logger';
+
+const logger = loggerFactory('growi:cli:HotkeyStroke');
+
+export default class HotkeyStroke {
+
+  constructor(stroke) {
+    this.stroke = stroke;
+    this.activeIndices = [];
+  }
+
+  get firstKey() {
+    return this.stroke[0];
+  }
+
+  /**
+   * Evaluate whether the specified key completes stroke or not
+   * @param {string} key
+   * @return T/F whether the specified key completes stroke or not
+   */
+  evaluate(key) {
+    if (key === this.firstKey) {
+      // add a new active index
+      this.activeIndices.push(0);
+    }
+
+    let isCompleted = false;
+    this.activeIndices = this.activeIndices
+      .map((index) => {
+        // return null when key does not match
+        if (key !== this.stroke[index]) {
+          return null;
+        }
+
+        const nextIndex = index + 1;
+
+        if (this.stroke.length <= nextIndex) {
+          isCompleted = true;
+          return null;
+        }
+
+        return nextIndex;
+      })
+      // exclude null
+      .filter(index => index != null);
+
+    // reset if completed
+    if (isCompleted) {
+      this.activeIndices = [];
+    }
+
+    logger.debug('activeIndices for [', this.stroke, '] => [', this.activeIndices, ']');
+
+    return isCompleted;
+  }
+
+}

+ 0 - 20
src/client/js/services/NavigationContainer.js

@@ -37,8 +37,6 @@ export default class NavigationContainer extends Container {
 
     this.openPageCreateModal = this.openPageCreateModal.bind(this);
     this.closePageCreateModal = this.closePageCreateModal.bind(this);
-
-    this.initHotkeys();
     this.initDeviceSize();
     this.initScrollEvent();
   }
@@ -50,24 +48,6 @@ export default class NavigationContainer extends Container {
     return 'NavigationContainer';
   }
 
-  initHotkeys() {
-    window.addEventListener('keydown', (event) => {
-      const target = event.target;
-
-      // ignore when target dom is input
-      const inputPattern = /^input|textinput|textarea$/i;
-      if (inputPattern.test(target.tagName) || target.isContentEditable) {
-        return;
-      }
-
-      if (event.key === 'c') {
-        // don't fire when not needed
-        if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey) {
-          this.setState({ isPageCreateModalShown: true });
-        }
-      }
-    });
-  }
 
   initDeviceSize() {
     const mdOrAvobeHandler = async(mql) => {

+ 3 - 0
src/client/styles/scss/_mirror_mode.scss

@@ -0,0 +1,3 @@
+body.mirror {
+  transform: scale(-1, 1);
+}

+ 9 - 11
src/client/styles/scss/_shortcuts.scss

@@ -1,15 +1,4 @@
 #shortcuts-modal {
-  @include media-breakpoint-only(xs) {
-    .modal-dialog {
-      width: 750px;
-    }
-  }
-  @include media-breakpoint-up(sm) {
-    table {
-      table-layout: fixed;
-    }
-  }
-
   h3 {
     margin-bottom: 1em;
   }
@@ -23,6 +12,15 @@
     }
   }
 
+  @include media-breakpoint-up(sm) {
+    table {
+      table-layout: fixed;
+      th {
+        width: 170px;
+      }
+    }
+  }
+
   // see http://coliss.com/articles/build-websites/operation/css/css-apple-keyboard-style-by-nrjmadan.html
   .key {
     /*Box Properties*/

+ 15 - 8
src/client/styles/scss/_staff_credit.scss

@@ -10,30 +10,37 @@
   margin: 10vh 10vw !important;
 
   // see https://css-tricks.com/old-timey-terminal-styling/
-  @mixin old-timey-terminal-styling() {
+  .credit-curtain {
+    padding-top: 80vh;
+
     text-shadow: 0 0 10px #c8c8c8;
     background-color: black;
     background-image: radial-gradient(rgba(50, 100, 100, 0.75), black 120%);
   }
+  &::after {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    content: '';
+    background: repeating-linear-gradient(0deg, rgba(black, 0.15), rgba(black, 0.15) 2px, transparent 2px, transparent 4px);
+  }
 
   h1,
   h2,
   h3,
   h4,
   h5,
-  h6 {
+  h6,
+  .dev-position,
+  .dev-name {
     font-family: 'Press Start 2P', $font-family-for-staff-credit;
     color: white;
   }
 
   $credit-length: -240em;
 
-  // see https://css-tricks.com/old-timey-terminal-styling/
-  .credit-curtain {
-    padding-top: 80vh;
-    @include old-timey-terminal-styling();
-  }
-
   h1 {
     font-size: 3em;
   }

+ 1 - 0
src/client/styles/scss/style-app.scss

@@ -42,6 +42,7 @@
 @import 'layout_kibela';
 @import 'login';
 @import 'me';
+@import 'mirror_mode';
 @import 'navbar';
 @import 'navbar_kibela';
 @import 'notification';

+ 1 - 2
src/server/views/layout/layout.html

@@ -97,8 +97,7 @@
 <div id="grw-fab-container"></div>
 {% endblock %}
 
-<!-- /#staff-credit -->
-<div id="staff-credit"></div>
+<div id="grw-hotkeys-manager"></div>
 
 {% include '../widget/system-version.html' %}
 

+ 16 - 14
src/server/views/modal/shortcuts.html

@@ -13,7 +13,7 @@
             <div class="col-lg-6">
               <h3><strong>{{ t('modal_shortcuts.global.title') }}</strong></h3>
 
-                <table class="table table-responsive">
+                <table class="table">
                   <tr>
                     <th>{{ t('modal_shortcuts.global.Open/Close shortcut help') }}:</th>
                     <td><span class="key cmd-key"></span> + <span class="key">/</span></td>
@@ -37,6 +37,17 @@
                       <span class="key key-small">B</span>&nbsp;<span class="key key-small">A</span>
                     </td>
                   </tr>
+                  <tr>
+                    <th>{{ t('modal_shortcuts.global.MirrorMode') }}:</th>
+                    <td>
+                      <a href="{{ t('modal_shortcuts.global.konami_code_url') }}" target="_blank">{{ t('modal_shortcuts.global.Konami Code') }}</a><br>
+                      <span class="key key-small">X</span>&nbsp;<span class="key key-small">X</span>
+                      <span class="key key-small">B</span>&nbsp;<span class="key key-small">B</span>
+                      <span class="key key-small">A</span><br><span class="key key-small">Y</span>
+                      <span class="key key-small">A</span>&nbsp;<span class="key key-small">Y</span>
+                      <span class="key key-small">↓</span>&nbsp;<span class="key key-small">←</span>
+                    </td>
+                  </tr>
                 </table>
             </div><!-- /.col-lg-6 -->
 
@@ -44,7 +55,7 @@
             <div class="col-lg-6">
               <h3><strong>{{ t('modal_shortcuts.editor.title') }}</strong></h3>
 
-              <table class="table table-responsive">
+              <table class="table">
                 <tr>
                   <th>{{ t('modal_shortcuts.editor.Indent') }}:</th>
                   <td><span class="key key-longer">Tab</span></td>
@@ -62,20 +73,10 @@
                   <td><span class="key cmd-key"></span> + <span class="key">D</span></td>
                 </tr>
               </table>
-            </div><!-- /.col-lg-6 -->
-          </div><!-- /.row -->
-        </div><!-- /.container -->
 
-        <div class="container">
-          <div class="row">
-            <div class="col-lg-6">
-              <h3><strong></strong></h3>
-            </div><!-- /.col-lg-6 -->
-
-            <div class="col-lg-6">
               <h3><strong>{{ t('modal_shortcuts.commentform.title') }}</strong></h3>
 
-              <table class="table table-responsive">
+              <table class="table">
                 <tr>
                   <th>{{ t('modal_shortcuts.commentform.Post') }}:</th>
                   <td><span class="key cmd-key"></span> + <span class="key key-longer">{% include '../widget/icon-keyboard-return-enter.html' %}</span></td>
@@ -85,10 +86,11 @@
                   <td><span class="key cmd-key"></span> + <span class="key">D</span></td>
                 </tr>
               </table>
-            </div><!-- /.col-lg-6 -->
 
+            </div><!-- /.col-lg-6 -->
           </div><!-- /.row -->
         </div><!-- /.container -->
+
       </div>
     </div>