Browse Source

Merge pull request #37 from weseek/imprv/page-title

Imprv/page title
Yuki Takei 9 years ago
parent
commit
473003b79e

+ 4 - 2
lib/views/page.html

@@ -14,7 +14,7 @@
 
 
     <div class="flex-title-line">
-      <h1 class="title flex-item-title" id="revision-path">{{ path|insertSpaceToEachSlashes }}</h1>
+      <h1 class="title flex-item-title" id="revision-path"></h1>
       {% if page %}
       <div class="flex-item-action">
         <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
@@ -28,12 +28,14 @@
       </div>
       {% endif %}
     </div>
+
+    <div id="revision-url" class="url-line"></div>
   </header>
   {% else %}
   {# trash/* #}
   <header id="page-header">
     <div class="flex-title-line">
-      <h1 class="title flex-item-title">{{ path|insertSpaceToEachSlashes }}</h1>
+      <h1 class="title flex-item-title"></h1>
       <div class="flex-item-action">
         <a href="#" title="Bookmark" class="bookmark-link" id="bookmark-button" data-csrftoken="{{ csrf() }}" data-bookmarked="0"><i class="fa fa-star-o"></i></a>
       </div>

+ 1 - 1
lib/views/page_list.html

@@ -16,7 +16,7 @@
   <header class="portal-header {% if page %}has-page{% endif %}">
     <div class="flex-title-line">
       <h1 class="title flex-item-title">
-        <span id="revision-path">{{ path|insertSpaceToEachSlashes }}</span>
+        <span id="revision-path"></span>
         {% if searchConfigured() && !isTopPage() && !isTrashPage() %}
         <form class="input-group search-input-group hidden-xs hidden-sm" data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索" id="search-listpage-form">
           <input type="text" class="search-listpage-input form-control" data-path="{{ path }}" id="search-listpage-input">

+ 1 - 0
package.json

@@ -103,6 +103,7 @@
     "normalize-path": "^2.1.1",
     "optimize-js-plugin": "0.0.4",
     "react": "^15.4.2",
+    "react-clipboard.js": "^1.0.1",
     "react-dom": "^15.4.2",
     "redis": "^2.7.1",
     "reveal.js": "~3.4.0",

+ 1 - 1
resource/css/_layout.scss

@@ -73,7 +73,7 @@
       }
 
       .content-main {
-        padding: 16px;
+        padding: 8px 16px;
       }
     }
 

+ 13 - 1
resource/css/_page.scss

@@ -65,6 +65,7 @@
       }
 
       .flex-title-line {
+        line-height: 1;
         display: -webkit-flex;
         display: flex;
         -webkit-align-items: center;
@@ -88,7 +89,12 @@
         margin-top: 0;
         margin-bottom: 0;
 
-        a:last-child {
+        // add spaces before and after separator
+        .separator::before, .separator::after {
+          content: " ";
+        }
+
+        a.last-path {
           color: #D1E2E4;
           opacity: .4;
 
@@ -96,6 +102,12 @@
             color: inherit;
           }
         }
+
+      }
+
+      .url-line {
+        color: #999;
+        font-size: 1rem;
       }
     }
 

+ 6 - 0
resource/js/app.js

@@ -9,6 +9,8 @@ import SearchPage       from './components/SearchPage';
 import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import SeenUserList     from './components/SeenUserList';
+import RevisionPath     from './components/Page/RevisionPath';
+import RevisionUrl      from './components/Page/RevisionUrl';
 //import PageComment  from './components/PageComment';
 
 if (!window) {
@@ -17,8 +19,10 @@ if (!window) {
 
 const mainContent = document.querySelector('#content-main');
 let pageId = null;
+let pagePath;
 if (mainContent !== null) {
   pageId = mainContent.attributes['data-page-id'].value;
+  pagePath = mainContent.attributes['data-path'].value;
 }
 
 // FIXME
@@ -43,6 +47,8 @@ const componentMappings = {
   //'revision-history': <PageHistory pageId={pageId} />,
   //'page-comment': <PageComment />,
   'seen-user-list': <SeenUserList />,
+  'revision-path': <RevisionPath pagePath={pagePath} />,
+  'revision-url': <RevisionUrl pagePath={pagePath} url={location.href} />,
 };
 
 Object.keys(componentMappings).forEach((key) => {

+ 49 - 0
resource/js/components/CopyButton.js

@@ -0,0 +1,49 @@
+import React from 'react';
+import ClipboardButton from 'react-clipboard.js';
+
+export default class CopyButton extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.showToolTip = this.showToolTip.bind(this);
+  }
+
+  showToolTip() {
+    const buttonId = `#${this.props.buttonId}`;
+    $(buttonId).tooltip('show');
+    setTimeout(() => {
+      $(buttonId).tooltip('hide');
+    }, 1000);
+  }
+
+  render() {
+    let style = Object.assign({
+      fontSize: "inherit",
+      padding: "2px",
+      border: 'none'
+    }, this.props.buttonStyle);
+
+    return (
+      <ClipboardButton className={this.props.buttonClassName}
+          button-id={this.props.buttonId} button-data-toggle="tooltip" button-title="copied!" button-data-placement="bottom" button-data-trigger="manual"
+          button-style={style}
+          data-clipboard-text={this.props.text} onSuccess={this.showToolTip}>
+
+        <i className={this.props.iconClassName}></i>
+      </ClipboardButton>
+    );
+  }
+}
+
+CopyButton.propTypes = {
+  text: React.PropTypes.string.isRequired,
+  buttonId: React.PropTypes.string.isRequired,
+  buttonClassName: React.PropTypes.string.isRequired,
+  buttonStyle: React.PropTypes.object,
+  iconClassName: React.PropTypes.string.isRequired,
+};
+CopyButton.defaultProps = {
+  buttonId: 'btnCopy',
+  buttonStyle: {},
+};

+ 0 - 64
resource/js/components/Page/PagePath.js

@@ -1,64 +0,0 @@
-import React from 'react';
-
-export default class PagePath extends React.Component {
-
-  // Original Crowi.linkPath
-  /*
-  Crowi.linkPath = function(revisionPath) {
-    var $revisionPath = revisionPath || '#revision-path';
-    var $title = $($revisionPath);
-    var pathData = $('#content-main').data('path');
-
-    if (!pathData) {
-      return ;
-    }
-
-    var realPath = pathData.trim();
-    if (realPath.substr(-1, 1) == '/') {
-      realPath = realPath.substr(0, realPath.length - 1);
-    }
-
-    var path = '';
-    var pathHtml = '';
-    var splittedPath = realPath.split(/\//);
-    splittedPath.shift();
-    splittedPath.forEach(function(sub) {
-      path += '/';
-      pathHtml += ' <a href="' + path + '">/</a> ';
-      if (sub) {
-        path += sub;
-        pathHtml += '<a href="' + path + '">' + sub + '</a>';
-      }
-    });
-    if (path.substr(-1, 1) != '/') {
-      path += '/';
-      pathHtml += ' <a href="' + path + '" class="last-path">/</a>';
-    }
-    $title.html(pathHtml);
-  };
-  */
-
-  linkPath(path) {
-    return path;
-  }
-
-  render() {
-    const page = this.props.page;
-    const shortPath = this.getShortPath(page.path);
-    const pathPrefix = page.path.replace(new RegExp(shortPath + '(/)?$'), '');
-
-    return (
-      <span className="page-path">
-        {pathPrefix}<strong>{shortPath}</strong>
-      </span>
-    );
-  }
-}
-
-PagePath.propTypes = {
-  page: React.PropTypes.object.isRequired,
-};
-
-PagePath.defaultProps = {
-  page: {},
-};

+ 85 - 0
resource/js/components/Page/RevisionPath.js

@@ -0,0 +1,85 @@
+import React from 'react';
+import CopyButton from '../CopyButton';
+
+export default class RevisionPath extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      pages: [],
+      isListPage: false,
+    };
+  }
+
+  componentWillMount() {
+    // whether list page or not
+    const isListPage = this.props.pagePath.match(/\/$/);
+    this.setState({ isListPage });
+
+    // generate pages obj
+    let splitted = this.props.pagePath.split(/\//);
+    splitted.shift();   // omit first element with shift()
+    if (splitted[splitted.length-1] === '') {
+      splitted.pop();   // omit last element with unshift()
+    }
+
+    let pages = [];
+    let parentPath = '/';
+    splitted.forEach((pageName) => {
+      pages.push({
+        pagePath: parentPath + pageName,
+        pageName: pageName,
+      });
+      parentPath += pageName + '/';
+    });
+
+    this.setState({ pages });
+  }
+
+  showToolTip() {
+    $('#btnCopy').tooltip('show');
+    setTimeout(() => {
+      $('#btnCopy').tooltip('hide');
+    }, 1000);
+  }
+
+  render() {
+    const pageLength = this.state.pages.length;
+
+    const afterElements = [];
+    this.state.pages.forEach((page, index) => {
+      const isLastElement = (index == pageLength-1);
+
+      // add elements
+      afterElements.push(
+        <span key={page.pagePath} className="path-segment">
+          <a href={page.pagePath}>{page.pageName}</a>
+        </span>);
+      afterElements.push(
+        <span key={page.pagePath+'/'} className="separator">
+          <a href={page.pagePath+'/'} className={(isLastElement && !this.state.isListPage) ? 'last-path' : ''}>/</a>
+        </span>
+      );
+    });
+
+    const buttonStyle = {
+      fontSize: '1.5rem'
+    }
+
+    return (
+      <span className="page-path">
+        <span className="separator">
+          <a href="/">/</a>
+        </span>
+        {afterElements}
+        <CopyButton buttonId="btnCopyRevisionPath" text={this.props.pagePath}
+            buttonClassName="btn btn-default" buttonStyle={buttonStyle} iconClassName="fa fa-clone text-muted" />
+      </span>
+    );
+  }
+}
+
+RevisionPath.propTypes = {
+  pagePath: React.PropTypes.string.isRequired,
+};

+ 28 - 0
resource/js/components/Page/RevisionUrl.js

@@ -0,0 +1,28 @@
+import React from 'react';
+import CopyButton from '../CopyButton';
+
+export default class RevisionUrl extends React.Component {
+
+  showToolTip() {
+    $('#btnCopy').tooltip('show');
+    setTimeout(() => {
+      $('#btnCopy').tooltip('hide');
+    }, 1000);
+  }
+
+  render() {
+    const text = this.props.pagePath + '\n' + this.props.url;
+    return (
+      <span>
+        {this.props.url}
+        <CopyButton buttonId="btnCopyRevisionUrl" text={text}
+            buttonClassName="btn btn-default" iconClassName="fa fa-link text-muted" />
+      </span>
+    );
+  }
+}
+
+RevisionUrl.propTypes = {
+  pagePath: React.PropTypes.string.isRequired,
+  url: React.PropTypes.string.isRequired,
+};

+ 0 - 35
resource/js/legacy/crowi.js

@@ -18,39 +18,6 @@ Crowi.createErrorView = function(msg) {
   $('#main').prepend($('<p class="alert-message error">' + msg + '</p>'));
 };
 
-Crowi.linkPath = function(revisionPath) {
-  var $revisionPath = revisionPath || '#revision-path';
-  var $title = $($revisionPath);
-  var pathData = $('#content-main').data('path');
-
-  if (!pathData) {
-    return ;
-  }
-
-  var realPath = pathData.trim();
-  if (realPath.substr(-1, 1) == '/') {
-    realPath = realPath.substr(0, realPath.length - 1);
-  }
-
-  var path = '';
-  var pathHtml = '';
-  var splittedPath = realPath.split(/\//);
-  splittedPath.shift();
-  splittedPath.forEach(function(sub) {
-    path += '/';
-    pathHtml += ' <a href="' + Crowi.escape(path) + '">/</a> ';
-    if (sub) {
-      path += sub;
-      pathHtml += '<a href="' + Crowi.escape(path) + '">' + Crowi.escape(sub) + '</a>';
-    }
-  });
-  if (path.substr(-1, 1) != '/') {
-    path += '/';
-    pathHtml += ' <a href="' + Crowi.escape(path) + '" class="last-path">/</a>';
-  }
-  $title.html(pathHtml);
-};
-
 Crowi.correctHeaders = function(contentId) {
   // h1 ~ h6 の id 名を補正する
   var $content = $(contentId || '#revision-body-content');
@@ -194,8 +161,6 @@ $(function() {
     }
   };
 
-  Crowi.linkPath();
-
   $('[data-toggle="popover"]').popover();
   $('[data-toggle="tooltip"]').tooltip();
   $('[data-tooltip-stay]').tooltip('show');

+ 32 - 0
yarn.lock

@@ -1184,6 +1184,14 @@ cli@~1.0.1:
     exit "0.1.2"
     glob "^7.1.1"
 
+clipboard@^1.4.0:
+  version "1.6.1"
+  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.6.1.tgz#65c5b654812466b0faab82dc6ba0f1d2f8e4be53"
+  dependencies:
+    good-listener "^1.2.0"
+    select "^1.1.2"
+    tiny-emitter "^1.0.0"
+
 cliui@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
@@ -1756,6 +1764,10 @@ delayed-stream@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
 
+delegate@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.1.2.tgz#1e1bc6f5cadda6cb6cbf7e6d05d0bcdd5712aebe"
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -2461,6 +2473,12 @@ globule@^1.0.0:
     lodash "~4.16.4"
     minimatch "~3.0.2"
 
+good-listener@^1.2.0:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
+  dependencies:
+    delegate "^3.1.2"
+
 google-auth-library@~0.9.7:
   version "0.9.10"
   resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-0.9.10.tgz#4993dc07bb4834b8ca0350213a6873a32c6051b9"
@@ -4606,6 +4624,12 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
+react-clipboard.js@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-1.0.1.tgz#56dea0547c13977cd1a1650fc740e0349aa90bdf"
+  dependencies:
+    clipboard "^1.4.0"
+
 react-dom@^15.4.2:
   version "15.5.4"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.4.tgz#ba0c28786fd52ed7e4f2135fe0288d462aef93da"
@@ -4990,6 +5014,10 @@ sax@~1.2.1:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828"
 
+select@^1.1.2:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+
 "semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@~5.3.0:
   version "5.3.0"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
@@ -5474,6 +5502,10 @@ timers-browserify@^2.0.2:
   dependencies:
     setimmediate "^1.0.4"
 
+tiny-emitter@^1.0.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.2.0.tgz#6dc845052cb08ebefc1874723b58f24a648c3b6f"
+
 tinycolor@0.x:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164"