From 593ca5f83b6912edfa188a9cb8f99d9c924d2aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20H=C3=A4usler?= Date: Tue, 31 Dec 2019 16:00:04 +0100 Subject: [PATCH] Refactor IPC selection messages (#1833) --- src/main/menu/actions/format.js | 10 +- src/main/menu/actions/paragraph.js | 117 ++++++++++++--------- src/muya/lib/contentState/formatCtrl.js | 1 + src/muya/lib/contentState/paragraphCtrl.js | 2 + src/renderer/store/editor.js | 112 +++++++++++++++++++- 5 files changed, 187 insertions(+), 55 deletions(-) diff --git a/src/main/menu/actions/format.js b/src/main/menu/actions/format.js index a9ac6904..34124b16 100644 --- a/src/main/menu/actions/format.js +++ b/src/main/menu/actions/format.js @@ -5,7 +5,7 @@ const MENU_ID_FORMAT_MAP = { strikeMenuItem: 'del', hyperlinkMenuItem: 'link', imageMenuItem: 'image', - mathMenuItem: 'inline_math' + inlineMathMenuItem: 'inline_math' } export const format = (win, type) => { @@ -19,12 +19,18 @@ export const format = (win, type) => { // NOTE: Don't use static `getMenuItemById` here, instead request the menu by // window id from `AppMenu` manager. +/** + * Update format menu entires from given state. + * + * @param {Electron.MenuItem} applicationMenu The application menu instance. + * @param {Object.} formats A object map with selected formats. + */ export const updateFormatMenu = (applicationMenu, formats) => { const formatMenuItem = applicationMenu.getMenuItemById('formatMenuItem') formatMenuItem.submenu.items.forEach(item => (item.checked = false)) formatMenuItem.submenu.items .forEach(item => { - if (item.id && formats.some(format => format.type === MENU_ID_FORMAT_MAP[item.id])) { + if (item.id && formats[MENU_ID_FORMAT_MAP[item.id]]) { item.checked = true } }) diff --git a/src/main/menu/actions/paragraph.js b/src/main/menu/actions/paragraph.js index 3a98400b..fa4890b9 100644 --- a/src/main/menu/actions/paragraph.js +++ b/src/main/menu/actions/paragraph.js @@ -17,13 +17,15 @@ const MENU_ID_MAP = { heading6MenuItem: 'h6', tableMenuItem: 'figure', codeFencesMenuItem: 'pre', + htmlBlockMenuItem: 'html', + mathBlockMenuItem: 'multiplemath', quoteBlockMenuItem: 'blockquote', orderListMenuItem: 'ol', bulletListMenuItem: 'ul', - taskListMenuItem: 'ul', + // taskListMenuItem: 'ul', paragraphMenuItem: 'p', horizontalLineMenuItem: 'hr', - frontMatterMenuItem: 'pre' + frontMatterMenuItem: 'frontmatter' // 'pre' } export const paragraph = (win, type) => { @@ -50,76 +52,95 @@ const setMultipleStatus = (applicationMenu, list, status) => { .forEach(item => (item.enabled = status)) } -const setCheckedMenuItem = (applicationMenu, affiliation) => { +const setCheckedMenuItem = (applicationMenu, { affiliation, isTable, isLooseListItem, isTaskList }) => { const paragraphMenuItem = applicationMenu.getMenuItemById('paragraphMenuEntry') paragraphMenuItem.submenu.items.forEach(item => (item.checked = false)) paragraphMenuItem.submenu.items.forEach(item => { if (!item.id) { return false } else if (item.id === 'looseListItemMenuItem') { - let checked = false - if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) { - checked = affiliation[0].children[0].isLooseListItem - } else if (affiliation.length >= 3 && affiliation[1].type === 'li') { - checked = affiliation[1].isLooseListItem - } - item.checked = checked - } else if (affiliation.some(b => { - if (b.type === 'ul') { - if (b.listType === 'bullet') { - return item.id === 'bulletListMenuItem' - } else { - return item.id === 'taskListMenuItem' - } - } else if (b.type === 'pre' && b.functionType) { - if (b.functionType === 'frontmatter') { - return item.id === 'frontMatterMenuItem' - } else if (/code$/.test(b.functionType)) { - return item.id === 'codeFencesMenuItem' - } else if (b.functionType === 'html') { - return item.id === 'htmlBlockMenuItem' - } else if (b.functionType === 'multiplemath') { - return item.id === 'mathBlockMenuItem' - } - } else if (b.type === 'figure' && b.functionType) { - if (b.functionType === 'table') { - return item.id === 'tableMenuItem' - } - } else { - return b.type === MENU_ID_MAP[item.id] + item.checked = !!isLooseListItem + } else if (Object.keys(affiliation).some(b => { + if (b === 'ul' && isTaskList) { + if (item.id === 'taskListMenuItem') { + return true + } + return false + } else if (isTable && item.id === 'tableMenuItem') { + return true + } else if (item.id === 'codeFencesMenuItem' && /code$/.test(b)) { + return true } + return b === MENU_ID_MAP[item.id] })) { item.checked = true } }) } -export const updateSelectionMenus = (applicationMenu, { start, end, affiliation }) => { - // format menu +/** + * Update paragraph menu entires from given state. + * + * @param {Electron.MenuItem} applicationMenu The application menu instance. + * @param {*} state The selection information. + */ +export const updateSelectionMenus = (applicationMenu, state) => { + const { + // Key/boolean object like "ul: true" of block elements that are selected. + // This may be an empty object when multiple block elements are selected. + affiliation, + isDisabled, + isMultiline, + isCodeFences, + isCodeContent + } = state + + // Reset format menu. const formatMenuItem = applicationMenu.getMenuItemById('formatMenuItem') formatMenuItem.submenu.items.forEach(item => (item.enabled = true)) - // handle menu checked - setCheckedMenuItem(applicationMenu, affiliation) - // handle disable - setParagraphMenuItemStatus(applicationMenu, true) - if ( - (start.block.functionType === 'cellContent' && end.block.functionType === 'cellContent') || - (start.type === 'span' && start.block.functionType === 'codeContent') || - (end.type === 'span' && end.block.functionType === 'codeContent') - ) { + // Handle menu checked. + setCheckedMenuItem(applicationMenu, state) + + // Reset paragraph menu. + setParagraphMenuItemStatus(applicationMenu, !isDisabled) + if (isDisabled) { + return + } + + if (isCodeFences) { setParagraphMenuItemStatus(applicationMenu, false) - if (start.block.functionType === 'codeContent' || end.block.functionType === 'codeContent') { - setMultipleStatus(applicationMenu, ['codeFencesMenuItem'], true) + // A code line is selected. + if (isCodeContent) { formatMenuItem.submenu.items.forEach(item => (item.enabled = false)) + + // TODO: Allow to transform to paragraph for other code blocks too but + // currently not supported by Muya. + // // Allow to transform to paragraph. + // if (affiliation.frontmatter) { + // setMultipleStatus(applicationMenu, ['frontMatterMenuItem'], true) + // } else if (affiliation.html) { + // setMultipleStatus(applicationMenu, ['htmlBlockMenuItem'], true) + // } else if (affiliation.multiplemath) { + // setMultipleStatus(applicationMenu, ['mathBlockMenuItem'], true) + // } else { + // setMultipleStatus(applicationMenu, ['codeFencesMenuItem'], true) + // } + + if (Object.keys(affiliation).some(b => /code$/.test(b))) { + setMultipleStatus(applicationMenu, ['codeFencesMenuItem'], true) + } } - } else if (start.key !== end.key) { + } else if (isMultiline) { formatMenuItem.submenu.items .filter(item => item.id && DISABLE_LABELS.includes(item.id)) .forEach(item => (item.enabled = false)) setMultipleStatus(applicationMenu, DISABLE_LABELS, false) - } else if (!affiliation.slice(0, 3).some(p => /ul|ol/.test(p.type))) { + } + + // Disable loose list item. + if (!affiliation.ul && !affiliation.ol) { setMultipleStatus(applicationMenu, ['looseListItemMenuItem'], false) } } diff --git a/src/muya/lib/contentState/formatCtrl.js b/src/muya/lib/contentState/formatCtrl.js index 47d98516..c2e149a3 100644 --- a/src/muya/lib/contentState/formatCtrl.js +++ b/src/muya/lib/contentState/formatCtrl.js @@ -148,6 +148,7 @@ const formatCtrl = ContentState => { if (!start || !end) { return { formats: [], tokens: [], neighbors: [] } } + const startBlock = this.getBlock(start.key) const formats = [] const neighbors = [] diff --git a/src/muya/lib/contentState/paragraphCtrl.js b/src/muya/lib/contentState/paragraphCtrl.js index cb65046e..dcdb9dd3 100644 --- a/src/muya/lib/contentState/paragraphCtrl.js +++ b/src/muya/lib/contentState/paragraphCtrl.js @@ -113,6 +113,8 @@ const paragraphCtrl = ContentState => { } } + // TODO: New created nestled list items missing "listType" key and value. + ContentState.prototype.handleListMenu = function (paraType, insertMode) { const { start, end, affiliation } = this.selectionChange(this.cursor) const { orderListDelimiter, bulletListMarker, preferLooseListItem } = this.muya.options diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index a0f74984..4c794a4c 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -995,16 +995,13 @@ const actions = { }) } - // TODO: We should only send a map of booleans to improve performance and not send - // the full change with all block elements with every change. - const { windowId } = global.marktext.env - ipcRenderer.send('mt::editor-selection-changed', windowId, changes) + ipcRenderer.send('mt::editor-selection-changed', windowId, createApplicationMenuState(changes)) }, SELECTION_FORMATS (_, formats) { const { windowId } = global.marktext.env - ipcRenderer.send('mt::update-format-menu', windowId, formats) + ipcRenderer.send('mt::update-format-menu', windowId, createSelectionFormatState(formats)) }, EXPORT ({ state }, { type, content, pageOptions }) { @@ -1241,4 +1238,109 @@ const trimTrailingNewlines = text => { return text.replace(/[\r?\n]+$/, '') } +/** + * Creates a object that contains the application menu state. + * + * @param {*} selection The selection. + * @returns A object that represents the application menu state. + */ +const createApplicationMenuState = ({ start, end, affiliation }) => { + const state = { + isDisabled: false, + // Whether multiple lines are selected. + isMultiline: start.key !== end.key, + // List information - a list must be selected. + isLooseListItem: false, + isTaskList: false, + // Whether the selection is code block like (math, html or code block). + isCodeFences: false, + // Whether a code block line is selected. + isCodeContent: false, + // Whether the selection contains a table. + isTable: false, + // Contains keys about the selection type(s) (string, boolean) like "ul: true". + affiliation: {} + } + const { isMultiline } = state + + // Get code block information from selection. + if ( + (start.block.functionType === 'cellContent' && end.block.functionType === 'cellContent') || + (start.type === 'span' && start.block.functionType === 'codeContent') || + (end.type === 'span' && end.block.functionType === 'codeContent') + ) { + // A code block like block is selected (code, math, ...). + state.isCodeFences = true + + // A code block line is selected. + if (start.block.functionType === 'codeContent' || end.block.functionType === 'codeContent') { + state.isCodeContent = true + } + } + + // Query list information. + if (affiliation.length >= 1 && /ul|ol/.test(affiliation[0].type)) { + const listBlock = affiliation[0] + state.affiliation[listBlock.type] = true + state.isLooseListItem = listBlock.children[0].isLooseListItem + state.isTaskList = listBlock.listType === 'task' + } else if (affiliation.length >= 3 && affiliation[1].type === 'li') { + const listItem = affiliation[1] + const listType = listItem.listItemType === 'order' ? 'ol' : 'ul' + state.affiliation[listType] = true + state.isLooseListItem = listItem.isLooseListItem + state.isTaskList = listItem.listItemType === 'task' + } + + // Search with block depth 3 (e.g. "ul -> li -> p" where p is the actually paragraph inside the list (item)). + for (const b of affiliation.slice(0, 3)) { + if (b.type === 'pre' && b.functionType) { + if (/frontmatter|html|multiplemath|code$/.test(b.functionType)) { + state.isCodeFences = true + state.affiliation[b.functionType] = true + } + break + } else if (b.type === 'figure' && b.functionType) { + if (b.functionType === 'table') { + state.isTable = true + state.isDisabled = true + } + break + } else if (isMultiline && /^h{1,6}$/.test(b.type)) { + // Multiple block elements are selected. + state.affiliation = {} + break + } else { + if (!state.affiliation[b.type]) { + state.affiliation[b.type] = true + } + } + } + + // Clean up + if (Object.getOwnPropertyNames(state.affiliation).length >= 2 && state.affiliation.p) { + delete state.affiliation.p + } + if ((state.affiliation.ul || state.affiliation.ol) && state.affiliation.li) { + delete state.affiliation.li + } + return state +} + +/** + * Creates a object that contains the formats selection state. + * + * @param {*} selection The selection. + * @returns A object that represents the formats menu state. + */ +const createSelectionFormatState = formats => { + // NOTE: Normally only one format can be selected but the selection is + // given as array by Muya. + const state = {} + for (const item of formats) { + state[item.type] = true + } + return state +} + export default { state, mutations, actions }