├── .eslintrc ├── .gitignore ├── .stylelintrc.json ├── renovate.json ├── assets ├── dark.css ├── light.css ├── moon-menu.ejs ├── styles.css └── moon-menu.js ├── .vscode └── settings.json ├── package.json ├── index.js ├── README.md └── LICENSE /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "hexo", 3 | "root": true 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | example/ 4 | yarn.lock -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard" 3 | } 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":preserveSemverRanges" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /assets/dark.css: -------------------------------------------------------------------------------- 1 | .moon-menu { 2 | --moon-cricle: #2b5278; 3 | --color: #9ab; 4 | --moon-item-bg-color: #182533; 5 | } 6 | -------------------------------------------------------------------------------- /assets/light.css: -------------------------------------------------------------------------------- 1 | .moon-menu { 2 | --moon-cricle: #607d8b; 3 | --color: #fff; 4 | --moon-item-bg-color: #000; 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "rules": { 4 | "linebreak-style": "off" 5 | } 6 | }, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/moon-menu.ejs: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-cake-moon-menu", 3 | "version": "2.5.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "p": "yarn --proxy=http://localhost:1080" 9 | }, 10 | "files": [ 11 | "assets", 12 | "index.js" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/jiangtj-lab/hexo-cake-moon-menu.git" 17 | }, 18 | "author": "Mr.J", 19 | "license": "LGPL-3.0", 20 | "bugs": { 21 | "url": "https://github.com/jiangtj-lab/hexo-cake-moon-menu/issues" 22 | }, 23 | "homepage": "https://github.com/jiangtj-lab/hexo-cake-moon-menu#readme", 24 | "dependencies": { 25 | "ejs": "^3.1.6", 26 | "hexo-extend-injector2": "^0.3.0", 27 | "hexo-util": "^2.4.0" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^7.22.0", 31 | "eslint-config-hexo": "^4.1.0", 32 | "stylelint": "^13.12.0", 33 | "stylelint-config-standard": "^21.0.0" 34 | }, 35 | "publishConfig": { 36 | "registry": "https://registry.npmjs.org/" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | .moon-menu, 2 | .moon-menu-bg, 3 | .moon-menu-content { 4 | position: fixed; 5 | right: 20px; 6 | bottom: 20px; 7 | width: 4em; 8 | height: 4em; 9 | z-index: 999; 10 | } 11 | 12 | .moon-menu-button { 13 | width: 4em; 14 | height: 4em; 15 | cursor: pointer; 16 | } 17 | 18 | .moon-menu-cricle { 19 | fill: var(--moon-cricle, #607d8b); 20 | opacity: 0.9; 21 | } 22 | 23 | .moon-menu-border { 24 | stroke: var(--moon-cricle, #607d8b); 25 | opacity: 0.9; 26 | stroke-width: 1px; 27 | fill: none; 28 | transform: rotate(-90deg); 29 | transform-origin: 50% 50%; 30 | } 31 | 32 | .moon-menu-icon { 33 | font-size: 2.5em; 34 | line-height: 1.6em; 35 | color: var(--color, #fff); 36 | text-align: center; 37 | } 38 | 39 | .moon-menu-icon i, 40 | .moon-menu-icon svg { 41 | transition: all 0.15s cubic-bezier(0, 0, 0.382, 1.618); 42 | } 43 | 44 | .moon-menu-icon.active i, 45 | .moon-menu-icon.active svg { 46 | transform: rotate(90deg); 47 | } 48 | 49 | .moon-menu-text { 50 | color: var(--color, #fff); 51 | font-weight: 400; 52 | text-align: center; 53 | line-height: 3.2em; 54 | font-size: 1.2em; 55 | display: none; 56 | } 57 | 58 | .moon-menu-item { 59 | position: absolute; 60 | top: 1em; 61 | left: 1em; 62 | width: 2.5em; 63 | height: 2.5em; 64 | border-radius: 50%; 65 | background-color: var(--moon-item-bg-color, #000); 66 | opacity: 0; 67 | display: block; 68 | transition: all 0.3s cubic-bezier(0, 0, 0.382, 1.618); 69 | z-index: -1; 70 | text-align: center; 71 | line-height: 2.5em; 72 | color: var(--color, #fff); 73 | cursor: pointer; 74 | } 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global hexo */ 2 | 'use strict'; 3 | 4 | const { join } = require('path'); 5 | const fs = require('fs'); 6 | const { Cache } = require('hexo-util'); 7 | const injector = require('hexo-extend-injector2')(hexo); 8 | const ejs = require('ejs'); 9 | 10 | const cache = new Cache(); 11 | 12 | hexo.extend.filter.register('after_init', () => { 13 | const faInline = hexo.extend.helper.get('fa_inline'); 14 | const fa = css => { 15 | if (!faInline) { 16 | return ``; 17 | } 18 | const data = css.split(' '); 19 | return faInline(data[1].substring(3), {prefix: data[0]}); 20 | }; 21 | 22 | const config = Object.assign({ 23 | back2top: { 24 | enable: true, 25 | icon: 'fas fa-chevron-up', 26 | order: -1 27 | }, 28 | back2bottom: { 29 | enable: true, 30 | icon: 'fas fa-chevron-down', 31 | order: '-2' 32 | } 33 | }, hexo.config.moon_menu); 34 | 35 | const moonMenuArr = Object.keys(config) 36 | .map(key => { 37 | const val = config[key]; 38 | val.id = val.id || key; 39 | return val; 40 | }) 41 | .map(item => { 42 | item.order = item.order || 0; 43 | if (item.enable === undefined) { 44 | item.enable = true; 45 | } 46 | return item; 47 | }) 48 | .filter(item => item.enable) 49 | .map(item => { 50 | item.icon = fa(item.icon); 51 | return item; 52 | }) 53 | .sort((a, b) => a.order - b.order); 54 | 55 | const light = fs.readFileSync(join(__dirname, 'assets/light.css')).toString(); 56 | const dark = fs.readFileSync(join(__dirname, 'assets/dark.css')).toString(); 57 | injector.register('variable', () => { 58 | return `${light}@media (prefers-color-scheme: dark) {${dark}}`; 59 | }); 60 | injector.register('variable', { 61 | env: 'light', 62 | value: light 63 | }); 64 | injector.register('variable', { 65 | env: 'dark', 66 | value: dark 67 | }); 68 | injector.register('style', join(__dirname, 'assets/styles.css')); 69 | injector.register('js', fs.readFileSync(join(__dirname, 'assets/moon-menu.js')).toString()); 70 | injector.register('bodyEnd', () => { 71 | return cache.apply('cache', () => { 72 | const template = fs.readFileSync(join(__dirname, 'assets/moon-menu.ejs')).toString(); 73 | return ejs.render(template, { icon: fa('fas fa-ellipsis-v'), menus: moonMenuArr }); 74 | }); 75 | }); 76 | }, 1); 77 | -------------------------------------------------------------------------------- /assets/moon-menu.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-var */ 2 | /* global document,window */ 3 | 'use strict'; 4 | ((window, document) => { 5 | 6 | const moonMenuListener = function() { 7 | // Get scroll percent 8 | const offsetHeight = document.documentElement.offsetHeight; 9 | const scrollHeight = document.documentElement.scrollHeight; 10 | const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop; 11 | let percent = Math.round(scrollTop / (scrollHeight - offsetHeight) * 100); 12 | if (percent > 100) percent = 100; 13 | 14 | const menuIcon = document.querySelector('.moon-menu-icon'); 15 | const menuText = document.querySelector('.moon-menu-text'); 16 | if (!percent) { 17 | percent = 0; 18 | menuIcon.style.display = 'block'; 19 | menuText.style.display = 'none'; 20 | } else { 21 | menuIcon.style.display = 'none'; 22 | menuText.style.display = 'block'; 23 | menuText.innerHTML = percent + '%'; 24 | } 25 | 26 | // Update strokeDasharray 27 | const length = 196; 28 | document.querySelector('.moon-menu-border').style.strokeDasharray = percent * length / 100 + ' ' + length; 29 | }; 30 | 31 | window.addEventListener('load', () => { 32 | moonMenuListener(); 33 | }); 34 | window.addEventListener('scroll', moonMenuListener); 35 | 36 | document.querySelector('.moon-menu-button').addEventListener('click', () => { 37 | document.querySelector('.moon-menu-icon').classList.toggle('active'); 38 | const items = document.querySelector('.moon-menu-items'); 39 | items.classList.toggle('active'); 40 | const childItems = document.querySelectorAll('.moon-menu-item'); 41 | if (items.classList.contains('active')) { 42 | for (let i = 0; i < childItems.length; i++) { 43 | childItems[i].style.top = -3 - 3 * i + 'em'; 44 | childItems[i].style.opacity = .9; 45 | } 46 | } else { 47 | for (let i = 0; i < childItems.length; i++) { 48 | childItems[i].style.top = '1em'; 49 | childItems[i].style.opacity = 0; 50 | } 51 | } 52 | }); 53 | 54 | const addClickListener = (id, call) => { 55 | const item = document.querySelector(id); 56 | if (item) { 57 | item.addEventListener('click', call); 58 | } 59 | }; 60 | 61 | addClickListener('#moon-menu-item-back2top', () => { 62 | window.scroll({ top: 0, behavior: 'smooth' }); 63 | }); 64 | 65 | addClickListener('#moon-menu-item-back2bottom', () => { 66 | const offsetHeight = document.documentElement.offsetHeight; 67 | const scrollHeight = document.documentElement.scrollHeight; 68 | window.scroll({ top: scrollHeight - offsetHeight, behavior: 'smooth' }); 69 | }); 70 | 71 | })(window, document); 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hexo-cake-moon-menu 2 | 3 | This plugin from [hexo-theme-inside](https://github.com/ike-c/hexo-theme-inside), thank ike-c. 4 | 5 | **If you've come here from my post, check out the [1.x](https://github.com/jiangtj-lab/hexo-cake-moon-menu/tree/1.x) branch code.** 6 | 7 |  8 | [:7.9.0-blue.svg)](https://theme-next.org) 9 | [](https://github.com/jiangtj/hexo-theme-cake) 10 | 11 | # Preview 12 |  13 |  14 |  15 |  16 | 17 | ## How to use 18 | 19 | ```bash 20 | yarn add hexo-cake-moon-menu 21 | ``` 22 | 23 | If you are using NexT theme version 7.8 or earlier, install version 2.1.2 24 | 25 | ```bash 26 | yarn add hexo-cake-moon-menu@2.1.2 27 | ``` 28 | 29 | ## Config 30 | 31 | In hexo `_config.yml` (here is default config, if don't change it, nothing need to append) 32 | 33 | ```yml 34 | moon_menu: 35 | back2top: 36 | enable: true 37 | icon: fas fa-chevron-up 38 | order: -1 39 | back2bottom: 40 | enable: true 41 | icon: fas fa-chevron-down 42 | order: -2 43 | ``` 44 | 45 | ## Custom 46 | 47 | It's easy to add new button in `moon-menu`. And here's an example about add gitter sidecar. 48 | 49 | 1. Add config 50 | ```yml 51 | moon_menu: 52 | chat: 53 | icon: fa fa-comments 54 | ``` 55 | 56 | 2. In `${hexo-dir}/scripts/any.js`, Add custom head 57 | ```js 58 | const path = require('path'); 59 | const injector = require('hexo-extend-injector2')(hexo); 60 | injector.register('body-end', ``); 66 | injector.register('body-end', ''); 67 | injector.register('js', path.resolve(hexo.base_dir, 'any/gitter.js')); 68 | ``` 69 | 70 | 3. In `${hexo-dir}/any/gitter.js`, create custom function 71 | ```js 72 | document.addEventListener('gitter-sidecar-instance-started', e => { 73 | // every button has it's id such as #moon-menu-item-