From 09835e30078d927c1ed371174714641ce7df42ff Mon Sep 17 00:00:00 2001 From: YeoKyung Yoon Date: Sat, 27 Aug 2022 15:21:50 +0900 Subject: [PATCH] Added sidebar toggler (#214) * feat: add sidebar slider * fix: fix visibility on mobile layout * fix visibility on mobile layout * change keyboard shortcut * change css property * fix: fix shortcuts to support Mac * fix shortcuts to support Mac * add comment to explain properties * fix: fix shortcuts * resolve merge conflict * fix: remove redundant property Co-authored-by: Peter Savchenko --- package.json | 1 + src/backend/views/components/sidebar.twig | 4 ++ src/frontend/js/modules/sidebar.js | 61 ++++++++++++++++++++- src/frontend/styles/components/sidebar.pcss | 46 ++++++++++++++++ src/frontend/svg/arrow-left.svg | 3 + yarn.lock | 14 ++--- 6 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 src/frontend/svg/arrow-left.svg diff --git a/package.json b/package.json index 4ad6319..2745b62 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest" }, "dependencies": { + "@codexteam/shortcuts": "^1.2.0", "@hawk.so/javascript": "^3.0.1", "@hawk.so/nodejs": "^3.1.2", "config": "^3.3.6", diff --git a/src/backend/views/components/sidebar.twig b/src/backend/views/components/sidebar.twig index a735b89..28d6131 100644 --- a/src/backend/views/components/sidebar.twig +++ b/src/backend/views/components/sidebar.twig @@ -52,4 +52,8 @@ + +
+ {{ svg('arrow-left') }} +
diff --git a/src/frontend/js/modules/sidebar.js b/src/frontend/js/modules/sidebar.js index dcb6d60..46d9782 100644 --- a/src/frontend/js/modules/sidebar.js +++ b/src/frontend/js/modules/sidebar.js @@ -1,10 +1,11 @@ import { Storage } from '../utils/storage'; +import Shortcut from '@codexteam/shortcuts'; /** * Local storage key */ const LOCAL_STORAGE_KEY = 'docs_sidebar_state'; - +const SIDEBAR_VISIBILITY_KEY = 'docs_sidebar_visibility'; /** * Section list item height in px @@ -31,6 +32,9 @@ export default class Sidebar { sectionList: 'docs-sidebar__section-list', sectionListItemActive: 'docs-sidebar__section-list-item--active', sidebarToggler: 'docs-sidebar__toggler', + sidebarSlider: 'docs-sidebar__slider', + sidebarCollapsed: 'docs-sidebar--collapsed', + sidebarAnimated: 'docs-sidebar--animated', sidebarContent: 'docs-sidebar__content', sidebarContentHidden: 'docs-sidebar__content--hidden', sidebarContentInvisible: 'docs-sidebar__content--invisible', @@ -45,14 +49,24 @@ export default class Sidebar { * Stores refs to HTML elements needed for correct sidebar work */ this.nodes = { + sidebar: null, sections: [], sidebarContent: null, toggler: null, + slider: null, }; this.sidebarStorage = new Storage(LOCAL_STORAGE_KEY); const storedState = this.sidebarStorage.get(); this.sectionsState = storedState ? JSON.parse(storedState) : {}; + + // Initialize localStorage that contains sidebar visibility + this.sidebarVisibilityStorage = new Storage(SIDEBAR_VISIBILITY_KEY); + // Get current sidebar visibility from storage + const storedVisibility = this.sidebarVisibilityStorage.get(); + + // Sidebar visibility + this.isVisible = storedVisibility !== 'false'; } /** @@ -62,11 +76,14 @@ export default class Sidebar { * @param {HTMLElement} moduleEl - module element */ init(settings, moduleEl) { + this.nodes.sidebar = moduleEl; this.nodes.sections = Array.from(moduleEl.querySelectorAll('.' + Sidebar.CSS.section)); this.nodes.sections.forEach(section => this.initSection(section)); this.nodes.sidebarContent = moduleEl.querySelector('.' + Sidebar.CSS.sidebarContent); this.nodes.toggler = moduleEl.querySelector('.' + Sidebar.CSS.sidebarToggler); this.nodes.toggler.addEventListener('click', () => this.toggleSidebar()); + this.nodes.slider = moduleEl.querySelector('.' + Sidebar.CSS.sidebarSlider); + this.nodes.slider.addEventListener('click', () => this.handleSliderClick()); this.ready(); } @@ -104,7 +121,7 @@ export default class Sidebar { const itemsCount = sectionList.children.length; - sectionList.style.maxHeight = `${ itemsCount * ITEM_HEIGHT }px`; + sectionList.style.maxHeight = `${itemsCount * ITEM_HEIGHT}px`; } /** @@ -168,12 +185,52 @@ export default class Sidebar { this.nodes.sidebarContent.classList.toggle(Sidebar.CSS.sidebarContentHidden); } + /** + * Initializes sidebar + * + * @returns {void} + */ + initSidebar() { + if (!this.isVisible) { + this.nodes.sidebar.classList.add(Sidebar.CSS.sidebarCollapsed); + } + + /** + * prevent sidebar animation on page load + * Since animated class contains transition, hiding will be animated with it + * To prevent awkward animation when visibility is set to false, we need to remove animated class + */ + setTimeout(() => { + this.nodes.sidebar.classList.add(Sidebar.CSS.sidebarAnimated); + }, 200); + + // add event listener to execute keyboard shortcut + // eslint-disable-next-line no-new + new Shortcut({ + name: 'CMD+.', + on: document.body, + callback: () => this.handleSliderClick(), + }); + } + + /** + * Slides sidebar + * + * @returns {void} + */ + handleSliderClick() { + this.isVisible = !this.isVisible; + this.sidebarVisibilityStorage.set(this.isVisible); + this.nodes.sidebar.classList.toggle(Sidebar.CSS.sidebarCollapsed); + } + /** * Displays sidebar when ready * * @returns {void} */ ready() { + this.initSidebar(); this.nodes.sidebarContent.classList.remove(Sidebar.CSS.sidebarContentInvisible); } } diff --git a/src/frontend/styles/components/sidebar.pcss b/src/frontend/styles/components/sidebar.pcss index ef807cf..1987f86 100644 --- a/src/frontend/styles/components/sidebar.pcss +++ b/src/frontend/styles/components/sidebar.pcss @@ -1,6 +1,34 @@ .docs-sidebar { width: 100vw; + &--animated { + .docs-sidebar__content { + transition: transform 200ms ease-in-out; + will-change: transform; + } + + .docs-sidebar__slider { + transition: transform 200ms ease-in-out; + will-change: transform; + } + } + + &--collapsed { + @media (--desktop) { + .docs-sidebar__content { + transform: translateX(-100%); + } + } + + .docs-sidebar__slider { + transform: translateX(20px); + + svg { + transform: rotate(180deg); + } + } + } + @media (--desktop) { width: var(--layout-sidebar-width); } @@ -188,6 +216,24 @@ } } + &__slider { + display: none; + position: fixed; + transform: translateX(calc(var(--layout-sidebar-width) + 20px)); + bottom: 20px; + width: 32px; + height: 32px; + border-radius: 8px; + cursor: pointer; + background-color: var(--color-link-hover); + + @media (--desktop) { + display: flex; + justify-content: center; + align-items: center; + } + } + &__logo { display: none; margin-top: auto; diff --git a/src/frontend/svg/arrow-left.svg b/src/frontend/svg/arrow-left.svg new file mode 100644 index 0000000..281b737 --- /dev/null +++ b/src/frontend/svg/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/yarn.lock b/yarn.lock index ec2ea42..9973699 100644 --- a/yarn.lock +++ b/yarn.lock @@ -922,6 +922,10 @@ resolved "https://registry.yarnpkg.com/@codexteam/misprints/-/misprints-1.0.0.tgz#e5a7dec7389fe0f176cd51a040d6dc9bdc252086" integrity sha512-R2IO1JmcaWCuWNPFVEAyar2HqQFuJwkeQUyVF0ovY4ip7z+VnVTYWxeYhCx7eZYEQCyXmcJooICQDihtn16lOA== +"@codexteam/shortcuts@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@codexteam/shortcuts/-/shortcuts-1.2.0.tgz#b8dd7396962b0bd845a5c8f8f19bc6119b520e19" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" @@ -937,7 +941,6 @@ "@csstools/selector-specificity@^2.0.0": version "2.0.2" resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36" - integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== "@discoveryjs/json-ext@^0.5.0": version "0.5.7" @@ -3073,7 +3076,6 @@ entities@^2.0.0: entities@^4.2.0, entities@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" - integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== envinfo@^7.7.3: version "7.8.1" @@ -3718,7 +3720,6 @@ function-bind@^1.1.1: function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" @@ -3733,7 +3734,6 @@ functional-red-black-tree@^1.0.1: functions-have-names@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== gensync@^1.0.0-beta.2: version "1.0.0-beta.2" @@ -3878,7 +3878,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.4 has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== has-flag@^3.0.0: version "3.0.0" @@ -5919,7 +5918,6 @@ qs@6.10.3: qs@^6.5.1: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== dependencies: side-channel "^1.0.4" @@ -6389,7 +6387,6 @@ shebang-regex@^3.0.0: shell-quote@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" - integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== side-channel@^1.0.4: version "1.0.4" @@ -7004,7 +7001,6 @@ untildify@^4.0.0: update-browserslist-db@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz#be06a5eedd62f107b7c19eb5bcefb194411abf38" - integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -7244,7 +7240,6 @@ yallist@^2.1.2: yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^1.10.0, yaml@^1.10.2: version "1.10.2" @@ -7272,7 +7267,6 @@ yargs-parser@^20.2.2: yargs-parser@^21.0.0: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== yargs-unparser@2.0.0: version "2.0.0"