diff --git a/doc/KEYBINDINGS.md b/doc/KEYBINDINGS.md index e346f22b..628f1be7 100644 --- a/doc/KEYBINDINGS.md +++ b/doc/KEYBINDINGS.md @@ -89,6 +89,7 @@ Here is an example: | ------------------- | ------------------------------------------- | | `formatStrong` | Set the font of the selected text to bold | | `formatEmphasis` | Set the font of the selected text to italic | +| `formatUnderline` | Change the selected text to underline | | `formatInlineCode` | Change the selected text to inline code | | `formatStrike` | Strike through the selected text | | `formatHyperlink` | Insert a hyperlink | diff --git a/src/main/menus/format.js b/src/main/menus/format.js index aca5664b..e1c9b6c0 100644 --- a/src/main/menus/format.js +++ b/src/main/menus/format.js @@ -20,6 +20,32 @@ export default { click (menuItem, browserWindow) { actions.format(browserWindow, 'em') } + }, { + id: 'underlineMenuItem', + label: 'Underline', + type: 'checkbox', + accelerator: keybindings.getAccelerator('formatUnderline'), + click (menuItem, browserWindow) { + actions.format(browserWindow, 'u') + } + }, { + type: 'separator' + }, { + id: 'superscriptMenuItem', + label: 'Superscript', + type: 'checkbox', + click (menuItem, browserWindow) { + actions.format(browserWindow, 'sup') + } + }, { + id: 'subscriptMenuItem', + label: 'Subscript', + type: 'checkbox', + click (menuItem, browserWindow) { + actions.format(browserWindow, 'sub') + } + }, { + type: 'separator' }, { id: 'inlineCodeMenuItem', label: 'Inline Code', diff --git a/src/main/shortcutHandler.js b/src/main/shortcutHandler.js index abd57883..5a077f8b 100644 --- a/src/main/shortcutHandler.js +++ b/src/main/shortcutHandler.js @@ -75,6 +75,7 @@ class Keybindings { // format menu ['formatStrong', 'CmdOrCtrl+B'], ['formatEmphasis', 'CmdOrCtrl+I'], + ['formatUnderline', 'CmdOrCtrl+U'], ['formatInlineCode', 'CmdOrCtrl+`'], ['formatInlineMath', 'Ctrl+M'], ['formatStrike', 'CmdOrCtrl+D'], diff --git a/src/muya/lib/assets/pngicon/format_underline/1.png b/src/muya/lib/assets/pngicon/format_underline/1.png new file mode 100755 index 00000000..f536e64f Binary files /dev/null and b/src/muya/lib/assets/pngicon/format_underline/1.png differ diff --git a/src/muya/lib/assets/pngicon/format_underline/2.png b/src/muya/lib/assets/pngicon/format_underline/2.png new file mode 100755 index 00000000..fd560fb7 Binary files /dev/null and b/src/muya/lib/assets/pngicon/format_underline/2.png differ diff --git a/src/muya/lib/assets/pngicon/format_underline/3.png b/src/muya/lib/assets/pngicon/format_underline/3.png new file mode 100755 index 00000000..c04588be Binary files /dev/null and b/src/muya/lib/assets/pngicon/format_underline/3.png differ diff --git a/src/muya/lib/config/index.js b/src/muya/lib/config/index.js index 4bebbeab..3fb14a53 100644 --- a/src/muya/lib/config/index.js +++ b/src/muya/lib/config/index.js @@ -167,7 +167,19 @@ export const FORMAT_MARKER_MAP = { 'inline_code': '`', 'strong': '**', 'del': '~~', - 'inline_math': '$' + 'inline_math': '$', + 'u': { + open: '', + close: '' + }, + 'sub': { + open: '', + close: '' + }, + 'sup': { + open: '', + close: '' + } } export const FORMAT_TYPES = ['strong', 'em', 'del', 'inline_code', 'link', 'image', 'inline_math'] diff --git a/src/muya/lib/contentState/formatCtrl.js b/src/muya/lib/contentState/formatCtrl.js index 544fb5fb..acf05c13 100644 --- a/src/muya/lib/contentState/formatCtrl.js +++ b/src/muya/lib/contentState/formatCtrl.js @@ -2,7 +2,7 @@ import selection from '../selection' import { tokenizer, generator } from '../parser/' import { FORMAT_MARKER_MAP, FORMAT_TYPES, URL_REG } from '../config' -const getOffset = (offset, { range: { start, end }, type, anchor, alt }) => { +const getOffset = (offset, { range: { start, end }, type, tag, anchor, alt }) => { const dis = offset - start const len = end - start switch (type) { @@ -19,6 +19,16 @@ const getOffset = (offset, { range: { start, end }, type, anchor, alt }) => { if (dis > len) return -2 * MARKER_LEN break } + case 'html_tag': { // handle underline, sup, sub + const OPEN_MARKER_LEN = FORMAT_MARKER_MAP[tag].open.length + const CLOSE_MARKER_LEN = FORMAT_MARKER_MAP[tag].close.length + if (dis < 0) return 0 + if (dis >= 0 && dis < OPEN_MARKER_LEN) return -dis + if (dis >= OPEN_MARKER_LEN && dis <= len - CLOSE_MARKER_LEN) return -OPEN_MARKER_LEN + if (dis > len - CLOSE_MARKER_LEN && dis <= len) return len - dis - OPEN_MARKER_LEN - CLOSE_MARKER_LEN + if (dis > len) return - OPEN_MARKER_LEN - CLOSE_MARKER_LEN + break + } case 'link': { const MARKER_LEN = 1 if (dis < MARKER_LEN) return 0 @@ -50,7 +60,8 @@ const clearFormat = (token, { start, end }) => { case 'strong': case 'del': case 'em': - case 'link': { + case 'link': + case 'html_tag': { // underline, sub, sup const { parent } = token const index = parent.indexOf(token) parent.splice(index, 1, ...token.children) @@ -90,6 +101,18 @@ const addFormat = (type, block, { start, end }) => { end.offset += MARKER.length break } + case 'sub': + case 'sup': + case 'u': { + const MARKER = FORMAT_MARKER_MAP[type] + const oldText = block.text + block.text = oldText.substring(0, start.offset) + + MARKER.open + oldText.substring(start.offset, end.offset) + + MARKER.close + oldText.substring(end.offset) + start.offset += MARKER.open.length + end.offset += MARKER.open.length + break + } case 'link': case 'image': { const oldText = block.text @@ -104,6 +127,13 @@ const addFormat = (type, block, { start, end }) => { } } +const checkTokenIsInlineFormat = token => { + const { type, tag } = token + if (FORMAT_TYPES.includes(type)) return true + if (type === 'html_tag' && /^(?:u|sub|sup)$/i.test(tag)) return true + return false +} + const formatCtrl = ContentState => { ContentState.prototype.selectionFormats = function ({ start, end } = selection.getCursorRange()) { if (!start || !end) { @@ -119,14 +149,14 @@ const formatCtrl = ContentState => { ;(function iterator (tks) { for (const token of tks) { if ( - FORMAT_TYPES.includes(token.type) && + checkTokenIsInlineFormat(token) && start.offset >= token.range.start && end.offset <= token.range.end ) { formats.push(token) } if ( - FORMAT_TYPES.includes(token.type) && + checkTokenIsInlineFormat(token) && ((start.offset >= token.range.start && start.offset <= token.range.end) || (end.offset >= token.range.start && end.offset <= token.range.end) || (start.offset <= token.range.start && token.range.end <= end.offset)) @@ -176,7 +206,10 @@ const formatCtrl = ContentState => { })) } - neighbors = type ? neighbors.filter(n => n.type === type) : neighbors + neighbors = type ? neighbors.filter(n => { + return n.type === type || + n.type === 'html_tag' && n.tag === type + }) : neighbors for (const neighbor of neighbors) { clearFormat(neighbor, { start, end }) @@ -263,8 +296,14 @@ const formatCtrl = ContentState => { start.delata = end.delata = 0 if (start.key === end.key) { const { formats, tokens, neighbors } = this.selectionFormats() - const currentFormats = formats.filter(format => format.type === type).reverse() - const currentNeightbors = neighbors.filter(format => format.type === type).reverse() + const currentFormats = formats.filter(format => { + return format.type === type || + format.type === 'html_tag' && format.tag === type + }).reverse() + const currentNeightbors = neighbors.filter(format => { + return format.type === type || + format.type === 'html_tag' && format.tag === type + }).reverse() // cache delata if (type === 'clear') { for (const neighbor of neighbors) { diff --git a/src/muya/lib/ui/formatPicker/config.js b/src/muya/lib/ui/formatPicker/config.js index 72876218..79ac3704 100644 --- a/src/muya/lib/ui/formatPicker/config.js +++ b/src/muya/lib/ui/formatPicker/config.js @@ -1,5 +1,6 @@ import strongIcon from '../../assets/pngicon/format_strong/2.png' import emphasisIcon from '../../assets/pngicon/format_emphasis/2.png' +import underlineIcon from '../../assets/pngicon/format_underline/2.png' import codeIcon from '../../assets/pngicon/code/2.png' import imageIcon from '../../assets/pngicon/format_image/2.png' import linkIcon from '../../assets/pngicon/format_link/2.png' @@ -14,6 +15,9 @@ const icons = [ }, { type: 'em', icon: emphasisIcon + }, { + type: 'u', + icon: underlineIcon }, { type: 'del', icon: strikeIcon diff --git a/src/muya/lib/ui/formatPicker/index.js b/src/muya/lib/ui/formatPicker/index.js index 3fde9851..05737e6f 100644 --- a/src/muya/lib/ui/formatPicker/index.js +++ b/src/muya/lib/ui/formatPicker/index.js @@ -62,7 +62,7 @@ class FormatPicker extends BaseFloat { } const iconWrapper = h(iconWrapperSelector, icon) let itemSelector = `li.item.${i.type}` - if (formats.some(f => f.type === i.type)) { + if (formats.some(f => f.type === i.type || f.type === 'html_tag' && f.tag === i.type)) { itemSelector += '.active' } return h(itemSelector, {