diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 3f949a255..41736d426 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -996,6 +996,9 @@ .cp-toolbar-tools { order: 7; } + .cp-toolbar-icon-pad_toc { + order: 8; + } .cp-toolbar-file { button { &.fa-plus { order: 0; } diff --git a/package-lock.json b/package-lock.json index 299dc473b..7b80098a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -393,12 +393,12 @@ } }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", + "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", "dev": true, "requires": { - "is-obj": "^1.0.0" + "is-obj": "^2.0.0" } }, "ecc-jsbn": { @@ -770,9 +770,9 @@ "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" }, "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true }, "is-typedarray": { @@ -1243,12 +1243,12 @@ } }, "postcss-selector-parser": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz", - "integrity": "sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", + "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", "dev": true, "requires": { - "dot-prop": "^4.1.1", + "dot-prop": "^5.2.0", "indexes-of": "^1.0.1", "uniq": "^1.0.1" } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 06509bf89..a431bf5e7 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -4104,5 +4104,11 @@ define([ }; }; + UIElements.isVisible = function (el, $container) { + var size = $container.outerHeight(); + var pos = el.getBoundingClientRect(); + return (pos.bottom < size) && (pos.y > 0); + }; + return UIElements; }); diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 572b779f0..b87dcc009 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1398,5 +1398,6 @@ "fm_restricted": "Vous n'avez pas accès à cet élément", "fm_emptyTrashOwned": "Votre corbeille contient des documents dont vous êtes propriétaire. Vous pouvez les supprimer de votre drive uniquement, ou les détruire pour tous les utilisateurs.", "fm_noResult": "Pas de résultat", - "support_formCategoryError": "Erreur : la catégorie est vide" + "support_formCategoryError": "Erreur : la catégorie est vide", + "pad_tocHide": "Plan" } diff --git a/www/common/translations/messages.ja.json b/www/common/translations/messages.ja.json index 3168088b3..cbf262772 100644 --- a/www/common/translations/messages.ja.json +++ b/www/common/translations/messages.ja.json @@ -177,5 +177,101 @@ "profileButton": "プロフィール", "profile_urlPlaceholder": "URL", "profile_avatar": "アバター", - "profile_upload": " 新しいアバターをアップロード" + "profile_upload": " 新しいアバターをアップロード", + "teams_table_generic_edit": "編集: フォルダとパッドの作成、変更、削除が可能。", + "teams_table_generic_view": "表示: フォルダとパッドへのアクセス(閲覧のみ)。", + "teams_table_generic_own": "チームの管理: チーム名とチームアバターの変更、所有者の追加または削除、チームのサブスクリプションの変更、チームの削除が可能。", + "teams_table_owners": "チームの管理", + "teams_table_generic_admin": "メンバーの管理: メンバーの招待および取り消し、メンバーに管理者までの権限の付与が可能。", + "teams_table_admins": "メンバーの管理", + "teams_table_generic": "権限一覧", + "teams_table": "権限", + "contacts_fetchHistory": "古い履歴を取得する", + "contacts_warning": "ここに入力したすべてのものは永続的であり、このパッドの現在および将来のすべてのユーザーが利用できます。機密情報の入力は推奨されません!", + "contacts_typeHere": "メッセージを入力...", + "team_listLoad": "開く", + "team_cat_drive": "ドライブ", + "team_cat_chat": "チャット", + "team_cat_members": "メンバー", + "team_cat_admin": "管理", + "adminPage": "管理", + "team_deleteButton": "削除", + "team_deleteHint": "チーム自体とチームが所有しているすべてのドキュメントを削除します。", + "team_deleteTitle": "チームの削除", + "team_avatarHint": "容量 500KB 以下 (png 、jpg 、jpeg 、gif)", + "team_avatarTitle": "チームアバター", + "team_nameHint": "チームの名前を設定します", + "team_nameTitle": "チーム名", + "team_members": "メンバー", + "team_owner": "所有者", + "team_admins": "管理者", + "editors": "編集者", + "viewers": "閲覧者", + "contacts_padTitle": "チャット", + "chatButton": "チャット", + "contacts_unmute": "ミュート解除", + "contacts_mute": "ミュート", + "contacts": "連絡先", + "share_linkOpen": "プレビュー", + "share_linkView": "表示", + "view": "表示", + "settings_exportError": "表示エラー", + "pad_mediatagPreview": "プレビュー", + "share_linkAccess": "アクセス権限", + "viewer": "閲覧者", + "reconnecting": "再接続中", + "synchronizing": "同期中", + "initializing": "初期化中...", + "yourself": "あなた", + "anonymous": "匿名", + "editor": "編集者", + "anonymousUser": "匿名の編集者", + "anonymousUsers": "匿名の編集者", + "typing": "編集中", + "team_infoLabel": "チームについて", + "support_cat_all": "全て", + "support_addAttachment": "添付ファイルを追加", + "support_attachments": "添付ファイル", + "support_cat_bug": "バグの報告", + "support_cat_data": "コンテンツの損失", + "support_cat_account": "ユーザーアカウント", + "support_formCategoryError": "エラー: カテゴリーが選択されていません", + "support_category": "カテゴリーを選択", + "support_languagesPreamble": "サポートチームは次の言語に対応可能です:", + "support_listHint": "管理者に送信されたチケットとその回答のリストは以下の通りです。閉じたチケットを再開することはできませんが、新しいチケットを作成することはできます。閉じたチケットは非表示にできます。", + "support_listTitle": "サポートチケット", + "settings_padNotifCheckbox": "コメント通知を無効化", + "settings_padNotifTitle": "コメント通知", + "notifications_dismissAll": "全て確認済みにする", + "notifications_cat_archived": "履歴", + "notifications_cat_friends": "連絡先リクエスト", + "notifications_dismiss": "確認済みにする", + "settings_autostoreMaybe": "手動 (確認しない)", + "settings_autostoreNo": "手動 (常に確認する)", + "settings_autostoreHint": "自動 あなたがアクセスしたすべてのパッドを、あなたの CryptDrive に保存します。
手動 (常に確認する) まだ保存していないパッドにアクセスした場合に、あなたの CryptDrive に保存するかどうか尋ねます。
手動 (確認しない) アクセス先のパッドがあなたの CryptDrive に自動的に保存されなくなります。保存オプションは表示されなくなります。", + "settings_userFeedback": "ユーザーフィードバックを有効化", + "settings_userFeedbackHint2": "あなたのパッドのコンテンツがサーバーと共有されることはありません。", + "settings_userFeedbackHint1": "CryptPad は、あなたの経験を向上させる方法を知るために、サーバーにいくつかの非常に基本的なフィードバックを提供します。 ", + "settings_userFeedbackTitle": "フィードバック", + "settings_autostoreYes": "自動", + "settings_importConfirm": "このブラウザで最近使用したパッドを、あなたのユーザーアカウントの CryptDrive にインポートしますか?", + "settings_importDone": "インポートが完了しました", + "settings_import": "インポート", + "settings_importTitle": "このブラウザでの最近のパッドをあなたの CryptDrive にインポートします", + "settings_trimHistoryHint": "ドライブと通知の履歴を削除して、ストレージ容量を節約します。これはパッドの履歴には影響しません。パッドの履歴は、プロパティダイアログから削除できます。", + "trimHistory_currentSize": "現在の履歴容量: {0}", + "support_cat_other": "その他", + "user_about": "CryptPad について", + "fc_delete": "ごみ箱へ移動", + "fc_remove": "削除", + "fc_restore": "復元", + "fc_delete_owned": "完全削除", + "creation_create": "作成", + "creation_password": "パスワードの追加", + "creation_expireMonths": "か月", + "creation_expireDays": "日", + "creation_expireHours": "時間", + "creation_expireFalse": "無制限", + "pad_wordCount": "単語数: {0}", + "teams_table_role": "権限" } diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 9b639d9b2..d63bf7cab 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1398,5 +1398,6 @@ "support_formCategoryError": "Error: category is empty", "fm_emptyTrashOwned": "Your trash contains documents you own. You can remove them from your drive only, or destroy them for all users.", "fm_restricted": "You do not have access", - "fm_noResult": "No results found" + "fm_noResult": "No results found", + "pad_tocHide": "Outline" } diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index bdaf4f5c7..b413eed6a 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -24,6 +24,33 @@ body.cp-app-pad { overflow: hidden; } + #cp-app-pad-toc { + @toc-level-indent: 15px; + + margin-top: 10px; + margin-left: 10px; + width: 200px; + color: @cryptpad_text_col; + h2 { + font-size: 1.5rem; + } + p { + margin-bottom: 5px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + a { + color: @cryptpad_text_col; + } + &.cp-pad-toc-2 { + margin-left: @toc-level-indent; + } + &.cp-pad-toc-3 { + margin-left: @toc-level-indent * 2; + } + } + } + .cke_toolbox_main { background-color: @colortheme_pad-toolbar-bg; .cke_toolbar { diff --git a/www/pad/comments.js b/www/pad/comments.js index 2fbd7711a..60466649c 100644 --- a/www/pad/comments.js +++ b/www/pad/comments.js @@ -5,8 +5,9 @@ define([ '/common/common-hash.js', '/common/hyperscript.js', '/common/common-interface.js', + '/common/common-ui-elements.js', '/customize/messages.js' -], function($, Sortify, Util, Hash, h, UI, Messages) { +], function($, Sortify, Util, Hash, h, UI, UIElements, Messages) { var Comments = {}; /* @@ -273,12 +274,6 @@ define([ ]); }; - var isVisible = function(el, $container) { - var size = $container.outerHeight(); - var pos = el.getBoundingClientRect(); - return (pos.bottom < size) && (pos.y > 0); - }; - var redrawComments = function(Env) { // Don't redraw if there were no change var str = Sortify(Env.comments || {}); @@ -558,7 +553,7 @@ define([ // Scroll into view if (!$last.length) { return; } - var visible = isVisible($last[0], Env.$inner); + var visible = UIElements.isVisible($last[0], Env.$inner); if (!visible) { $last[0].scrollIntoView(); } }; @@ -574,7 +569,7 @@ define([ focusContent(); - var visible = isVisible(div, Env.$container); + var visible = UIElements.isVisible(div, Env.$container); if (!visible) { div.scrollIntoView(); } }); diff --git a/www/pad/inner.html b/www/pad/inner.html index 76def97dc..342d5c4db 100644 --- a/www/pad/inner.html +++ b/www/pad/inner.html @@ -45,7 +45,6 @@
-
diff --git a/www/pad/inner.js b/www/pad/inner.js index 73dc00e75..30499f618 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -42,6 +42,7 @@ define([ '/common/common-hash.js', '/common/common-util.js', '/common/common-interface.js', + '/common/common-ui-elements.js', '/common/hyperscript.js', '/bower_components/chainpad/chainpad.dist.js', '/customize/application_config.js', @@ -69,6 +70,7 @@ define([ Hash, Util, UI, + UIElements, h, ChainPad, AppConfig, @@ -424,6 +426,41 @@ define([ }); }; + var addTOCHideBtn = function(framework, $toc) { + // Expand / collapse the toolbar + var onClick = function(visible) { + framework._.sfCommon.setAttribute(['pad', 'showTOC'], visible); + }; + framework._.sfCommon.getAttribute(['pad', 'showTOC'], function(err, data) { + var state = false; + if (($(window).height() >= 800 || $(window).width() >= 800) && + (typeof(data) === "undefined" || data)) { + state = true; + $toc.show(); + } else { + $toc.hide(); + } + var $tocButton = framework._.sfCommon.createButton('', true, { + drawer: false, + text: Messages.pad_tocHide, + name: 'pad_toc', + icon: 'fa-list-ul', + }, function () { + $tocButton.removeClass('cp-toolbar-button-active'); + $toc.toggle(); + state = $toc.is(':visible'); + if (state) { + $tocButton.addClass('cp-toolbar-button-active'); + } + onClick(state); + }); + framework._.toolbar.$bottomL.append($tocButton); + if (state) { + $tocButton.addClass('cp-toolbar-button-active'); + } + }); + }; + var displayMediaTags = function(framework, dom, mediaTagMap) { setTimeout(function() { // Just in case var tags = dom.querySelectorAll('media-tag:empty'); @@ -537,6 +574,9 @@ define([ $container: $('#cp-app-pad-comments') }); + var $toc = $('#cp-app-pad-toc'); + addTOCHideBtn(framework, $toc); + // My cursor var cursor = module.cursor = Cursor(inner); @@ -607,6 +647,34 @@ define([ }, 500); // 500ms to make sure it is sent after chainpad sync }; + var updateTOC = Util.throttle(function () { + var toc = []; + $inner.find('h1, h2, h3').each(function (i, el) { + toc.push({ + level: Number(el.tagName.slice(1)), + el: el, + title: Util.stripTags($(el).text()) + }); + }); + var content = [h('h2', Messages.markdown_toc)]; + toc.forEach(function (obj) { + // Only include level 2 headings + var level = obj.level; + var a = h('a.cp-pad-toc-link', { + href: '#', + }); + $(a).click(function (e) { + e.preventDefault(); + e.stopPropagation(); + if (!obj.el || UIElements.isVisible(obj.el, $inner)) { return; } + obj.el.scrollIntoView(); + }); + a.innerHTML = obj.title; + content.push(h('p.cp-pad-toc-'+level, a)); + }); + $toc.html('').append(content); + }); + // apply patches, and try not to lose the cursor in the process! framework.onContentUpdate(function(hjson) { if (!Array.isArray(hjson)) { throw new Error(Messages.typeError); } @@ -666,6 +734,8 @@ define([ } comments.onContentUpdate(); + + updateTOC(); }); framework.setTextContentGetter(function() { @@ -877,8 +947,12 @@ define([ framework.localChange(); updateCursor(); editor.fire('cp-wc'); // Update word count + updateTOC(); + }); + editor.on('change', function () { + framework.localChange(); + updateTOC(); }); - editor.on('change', framework.localChange); var wordCount = h('span.cp-app-pad-wordCount'); $('.cke_toolbox_main').append(wordCount); @@ -1077,6 +1151,7 @@ define([ var $ckeToolbar = $('#cke_1_top').find('.cke_toolbox_main'); $mainContainer.prepend($ckeToolbar.addClass('cke_reset_all')); $contentContainer.append(h('div#cp-app-pad-comments')); + $contentContainer.prepend(h('div#cp-app-pad-toc')); $ckeToolbar.find('.cke_button__image_icon').parent().hide(); }).nThen(waitFor()); diff --git a/www/pad/links.js b/www/pad/links.js index 6445de68b..a79183fa6 100644 --- a/www/pad/links.js +++ b/www/pad/links.js @@ -1,19 +1,28 @@ define([ 'jquery', '/common/hyperscript.js', + '/common/common-ui-elements.js', '/customize/messages.js' -], function ($, h, Messages) { +], function ($, h, UIElements, Messages) { var onLinkClicked = function (e, inner) { var $target = $(e.target); if (!$target.is('a')) { return; } var href = $target.attr('href'); - if (!href || href[0] === '#') { return; } + if (!href) { return; } + var $inner = $(inner); + e.preventDefault(); e.stopPropagation(); + if (href[0] === '#') { + var anchor = $inner.find(href); + if (!anchor.length) { return; } + anchor[0].scrollIntoView(); + return; + } + var $iframe = $('html').find('iframe').contents(); - var $inner = $(inner); var rect = e.target.getBoundingClientRect(); var rect0 = inner.getBoundingClientRect();