diff --git a/apps/mobile/src/views/Editor/EditorHeader.js b/apps/mobile/src/views/Editor/EditorHeader.js index 91011c9db..d465c7017 100644 --- a/apps/mobile/src/views/Editor/EditorHeader.js +++ b/apps/mobile/src/views/Editor/EditorHeader.js @@ -31,6 +31,7 @@ import { setIntent } from './Functions'; import HistoryComponent from './HistoryComponent'; +import tiny from './tiny/tiny'; let handleBack; let tapCount = 0; @@ -44,9 +45,7 @@ const EditorHeader = () => { setColors(colors); }, [colors.bg]); - useEffect(() => { - post('tablet', DDS.isLargeTablet()); - }, [deviceMode]); + useEffect(() => { @@ -179,7 +178,7 @@ const EditorHeader = () => { onPress={async () => { let note = getNote() && db.notes.note(getNote().id).data; if (editing.isFocused) { - post('blur'); + tiny.call(EditorWebView,tiny.blur) await sleep(500); editing.isFocused = true; } diff --git a/apps/mobile/src/views/Editor/EditorRoot.js b/apps/mobile/src/views/Editor/EditorRoot.js index 8b139c423..fee8041d3 100644 --- a/apps/mobile/src/views/Editor/EditorRoot.js +++ b/apps/mobile/src/views/Editor/EditorRoot.js @@ -66,7 +66,7 @@ const EditorRoot = () => { await loadNote(item); InteractionManager.runAfterInteractions(() => { Keyboard.addListener('keyboardDidShow', () => { - console.log(editing.currentlyEditing,'current') + if (!editing.movedAway) { editing.isFocused = true; } diff --git a/apps/mobile/src/views/Editor/EditorTitle.js b/apps/mobile/src/views/Editor/EditorTitle.js index e30537320..b8ace35af 100644 --- a/apps/mobile/src/views/Editor/EditorTitle.js +++ b/apps/mobile/src/views/Editor/EditorTitle.js @@ -70,7 +70,6 @@ export const EditorTitle = () => { placeholder="Title Here" onChangeText={(text) => { setTitle(text); - post('title', text); _onMessage({ nativeEvent: { data: JSON.stringify({ diff --git a/apps/mobile/src/views/Editor/Functions.js b/apps/mobile/src/views/Editor/Functions.js index f77e72314..d79cb4cf7 100644 --- a/apps/mobile/src/views/Editor/Functions.js +++ b/apps/mobile/src/views/Editor/Functions.js @@ -17,8 +17,10 @@ import {MMKV} from '../../utils/mmkv'; import {sideMenuRef, tabBarRef} from '../../utils/Refs'; import {normalize} from '../../utils/SizeUtils'; import {sleep, timeConverter} from '../../utils/TimeUtils'; +import tiny from './tiny/tiny'; export let EditorWebView = createRef(); +export const editorTitleInput = createRef(); export const params = 'platform=' + Platform.OS; export const sourceUri = @@ -33,13 +35,13 @@ export const injectedJS = ` setTimeout(() => { },100); `; -let webviewOK = true; +let webviewOK = true; let noteEdited = false; let note = null; let id = null; let content = { data: [], - type: 'delta', + type: 'tiny', }; let title = ''; let saveCounter = 0; @@ -53,7 +55,7 @@ export function setWebviewInit(init) { } export function getWebviewInit(init) { - return webviewInit + return webviewInit; } export function setIntent() { @@ -73,7 +75,7 @@ export function setColors(colors) { if (note && note.color && !DDS.isLargeTablet()) { theme.shade = hexToRGBA(COLORS_NOTE[note.color], 0.15); } - post('theme', theme); + tiny.call(EditorWebView, tiny.updateTheme(JSON.stringify(theme))); } export function isNotedEdited() { @@ -91,15 +93,8 @@ export function clearTimer() { } } -export const INJECTED_JAVASCRIPT = (premium) => - `(function() { - setTimeout(() => { - loadAction(${premium}); - },100) - })();`; - export const CHECK_STATUS = (premium) => - `(function() { + `(function() { setTimeout(() => { let msg = JSON.stringify({ data: true, @@ -108,7 +103,7 @@ export const CHECK_STATUS = (premium) => window.ReactNativeWebView.postMessage(msg) },100) -})();`; +})();`; export function getNote() { return note; @@ -160,31 +155,31 @@ function clearNote() { id = null; content = { data: [], - type: 'delta', + type: 'tiny', }; } let currentEditingTimer = null; - let webviewTimer = null; export const loadNote = async (item) => { editing.currentlyEditing = true; - post('blur'); + tiny.call(EditorWebView, tiny.blur); if (item && item.type === 'new') { await clearEditor(); clearNote(); noteEdited = false; id = null; - await sleep(10); + await sleep(20); if (Platform.OS === 'android') { textInput.current?.focus(); - post('focusTitle'); + console.log('focus'); + tiny.call(EditorWebView, tiny.focusEditor); EditorWebView.current?.requestFocus(); } else { - post('focusTitle'); + editorTitleInput.current?.focus(); } if (!webviewInit) { EditorWebView.current?.reload(); @@ -207,28 +202,26 @@ export const loadNote = async (item) => { }; const checkStatus = () => { - webviewOK = false + webviewOK = false; EditorWebView.current?.injectJavaScript(CHECK_STATUS()); - clearTimeout(webviewTimer) - console.log('webview ok',webviewOK); + clearTimeout(webviewTimer); webviewTimer = setTimeout(() => { if (!webviewOK) { webviewInit = false; EditorWebView = createRef(); - console.log("webview failed to load") - eSendEvent("webviewreset") + eSendEvent('webviewreset'); } else { - console.log("webview is running",webviewOK) + console.log('webview is running', webviewOK); } - },3000) -} + }, 5000); +}; export function setIntentNote(item) { id = null; intent = true; content = { data: item.data, - type: 'delta', + type: 'tiny', }; } @@ -243,14 +236,15 @@ export const _onMessage = async (evt) => { } switch (message.type) { case 'history': - eSendEvent('historyEvent', message); + eSendEvent('historyEvent', message.value); break; - case 'delta': + case 'change': content = message; onNoteChange(); break; case 'title': noteEdited = true; + console.log('title', message.value); title = message.value; eSendEvent('editorScroll', { title: message.value, @@ -278,11 +272,15 @@ export const _onMessage = async (evt) => { loadNoteInEditor(); break; case 'running': - webviewOK = false; - break; + webviewOK = true; + break; case 'focus': + console.log(message.value, 'value'); editing.focusType = message.value; break; + case 'selectionchange': + eSendEvent('onSelectionChange', message.value); + break; default: break; } @@ -302,7 +300,7 @@ function onNoteChange() { export async function clearEditor() { clearTimer(); - post('reset'); + tiny.call(EditorWebView, tiny.reset); if (noteEdited && id) { await saveNote(false); } @@ -316,12 +314,12 @@ export async function clearEditor() { redo: 0, }); saveCounter = 0; - post('dateEdited', ''); - post('saving', ''); + tiny.call(EditorWebView, tiny.updateDateEdited('')); + tiny.call(EditorWebView, tiny.updateSavingState('')); + clearNote(); } - async function setNoteInEditorAfterSaving(oldId, currentId) { if (oldId !== currentId) { id = currentId; @@ -420,21 +418,19 @@ export async function saveNote() { id: id, }); let n = db.notes.note(id)?.data?.dateEdited; - post('dateEdited', timeConverter(n)); - post('saving', 'Saved'); + tiny.call(EditorWebView, tiny.updateDateEdited(timeConverter(n))); + tiny.call(EditorWebView, tiny.updateSavingState('Saved')); } catch (e) { console.log(e); } } export async function onWebViewLoad(premium, colors) { - - EditorWebView.current?.injectJavaScript(INJECTED_JAVASCRIPT(premium)); if (!checkNote()) { - post('blur'); + tiny.call(EditorWebView, tiny.reset); Platform.OS === 'android' ? EditorWebView.current?.requestFocus() : null; } - post('blur'); + tiny.call(EditorWebView, tiny.reset); setColors(colors); await restoreEditorState(); } @@ -459,13 +455,12 @@ const loadNoteInEditor = async () => { if (!webviewInit) return; saveCounter = 0; if (note?.id) { - post('title', title); + tiny.call(EditorWebView, tiny.setTitle(title)); intent = false; + tiny.call(EditorWebView, tiny.html(content.data)); setColors(); - post('delta', content.data); - await sleep(10); - post('dateEdited', timeConverter(note.dateEdited)); + tiny.call(EditorWebView, tiny.updateDateEdited(note.dateEdited)); + tiny.call(EditorWebView, tiny.updateSavingState('Saved')); } - await sleep(10); - post('clearHistory'); + tiny.call(EditorWebView, tiny.clearHistory); }; diff --git a/apps/mobile/src/views/Editor/HistoryComponent.js b/apps/mobile/src/views/Editor/HistoryComponent.js index d34227f06..2ca2bb684 100644 --- a/apps/mobile/src/views/Editor/HistoryComponent.js +++ b/apps/mobile/src/views/Editor/HistoryComponent.js @@ -1,60 +1,58 @@ import React, {useEffect, useState} from 'react'; import {ActionIcon} from '../../components/ActionIcon'; import {useTracked} from '../../provider'; -import { eSubscribeEvent, eUnSubscribeEvent } from '../../services/EventManager'; -import {post,} from './Functions'; - +import {eSubscribeEvent, eUnSubscribeEvent} from '../../services/EventManager'; +import {EditorWebView, post} from './Functions'; +import tiny from './tiny/tiny'; const HistoryComponent = () => { - const [state] = useTracked(); - const {colors} = state; - const [historyState,setHistoryState] = useState({ - undo:0, - redo:0 - }); + const [state] = useTracked(); + const {colors} = state; + const [historyState, setHistoryState] = useState({ + undo: false, + redo: false, + }); - const onHistoryChange = (data) => { - setHistoryState(data); - } + const onHistoryChange = (data) => { + setHistoryState(data); + }; - useEffect(() => { - eSubscribeEvent('historyEvent',onHistoryChange); + useEffect(() => { + eSubscribeEvent('historyEvent', onHistoryChange); - return () => { - eUnSubscribeEvent('historyEvent',onHistoryChange); - } - },[]) + return () => { + eUnSubscribeEvent('historyEvent', onHistoryChange); + }; + }, []); - return ( - <> - - { - if (historyState.undo === 0) return; - post('undo'); - }} - /> - { - if (historyState.redo === 0) return; - post('redo'); - }} - /> - - - ); + return ( + <> + { + if (!historyState.undo) return; + tiny.call(EditorWebView, tiny.undo); + }} + /> + { + if (!historyState.redo) return; + tiny.call(EditorWebView, tiny.redo); + }} + /> + + ); }; export default HistoryComponent; diff --git a/apps/mobile/src/views/Editor/index.js b/apps/mobile/src/views/Editor/index.js index 9ba73b2a3..c5321b026 100755 --- a/apps/mobile/src/views/Editor/index.js +++ b/apps/mobile/src/views/Editor/index.js @@ -1,4 +1,6 @@ import React, {useEffect, useState} from 'react'; +import {ScrollView} from 'react-native'; +import {View} from 'react-native'; import {Platform, TextInput} from 'react-native'; import WebView from 'react-native-webview'; import {notesnook} from '../../../e2e/test.ids'; @@ -11,9 +13,11 @@ import { ToastEvent, } from '../../services/EventManager'; import {getCurrentColors} from '../../utils/Colors'; +import {SIZE} from '../../utils/SizeUtils'; import {sleep} from '../../utils/TimeUtils'; import EditorHeader from './EditorHeader'; import { + editorTitleInput, EditorWebView, injectedJS, onWebViewLoad, @@ -22,12 +26,13 @@ import { _onMessage, _onShouldStartLoadWithRequest, } from './Functions'; +import EditorToolbar from './tiny/toolbar'; const source = Platform.OS === 'ios' ? {uri: sourceUri} : { - uri: 'file:///android_asset/texteditor.html', + uri: 'file:///android_asset/index.html', baseUrl: 'file:///android_asset/', }; @@ -50,7 +55,6 @@ const Editor = React.memo( }; const onResetRequested = async () => { - console.log('resetting now'); setResetting(true); await sleep(3000); ToastEvent.show('Editor has recovered from crash.', 'success'); @@ -64,7 +68,7 @@ const Editor = React.memo( return () => { eUnSubscribeEvent('webviewreset', onResetRequested); }; - },[loading]); + }, [loading]); return resetting || loading ? ( - - { - ToastEvent.show('Editor Load Error', 'error'); - }} - javaScriptEnabled={true} - focusable={true} - keyboardDisplayRequiresUserAction={false} - injectedJavaScript={Platform.OS === 'ios' ? injectedJS : null} - onShouldStartLoadWithRequest={_onShouldStartLoadWithRequest} - cacheMode="LOAD_DEFAULT" - cacheEnabled={false} - domStorageEnabled={true} - scrollEnabled={false} + + bouncesZoom={false} + showsVerticalScrollIndicator={false} + style={{ + height: '100%', + width: '100%', + }} + nestedScrollEnabled + contentContainerStyle={{ + width: '100%', + height: '100%', + }}> + + { + console.log('error', event.nativeEvent); + ToastEvent.show('Editor Load Error', 'error'); + }} + onRenderProcessGone={(event) => { + console.log('error', event.nativeEvent); + onResetRequested(); + ToastEvent.show('Editor Render Process Gone', 'error'); + }} + javaScriptEnabled={true} + focusable={true} + keyboardDisplayRequiresUserAction={false} + injectedJavaScript={Platform.OS === 'ios' ? injectedJS : null} + onShouldStartLoadWithRequest={_onShouldStartLoadWithRequest} + cacheMode="LOAD_DEFAULT" + cacheEnabled={false} + domStorageEnabled={true} + scrollEnabled={false} + bounces={false} + allowFileAccess={true} + scalesPageToFit={true} + allowingReadAccessToURL={Platform.OS === 'android' ? true : null} + allowFileAccessFromFileURLs={true} + allowUniversalAccessFromFileURLs={true} + originWhitelist={['*']} + source={source} + style={style} + onMessage={_onMessage} + /> + + ); }, diff --git a/apps/mobile/src/views/Editor/tiny/tiny.js b/apps/mobile/src/views/Editor/tiny/tiny.js new file mode 100644 index 000000000..9a9039572 --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/tiny.js @@ -0,0 +1,153 @@ +const reset = ` +(() => { +editor.undoManager.clear(); +editor.('', 'custom'); +document.activeElement.blur(); +window.blur(); +editor.resetContent(); +})(); +`; +export const keyboardStateChanged = `(() => { + var range = editor.getSelection(); + if (range) { + if (range.length == 0) { + let correction = isTablet ? 215 : 60; + setTimeout(() => { + document.querySelector('.app-main').scrollTo({ + top: + editor.getBounds(editor.getSelection().index).bottom - + (window.innerHeight - correction), + behavior: 'smooth', + }); + }, 200); + } + } +})(); +`; +const blur = ` + document.activeElement.blur(); + window.blur(); + +`; + +const updateDateEdited = (value) => ` + (() => { + info = document.querySelector(infoBar); + info.querySelector('#infodate').innerText = "${value}"; + info.querySelector('#infowords').innerText = + editor.plugins.wordcount.getCount() + " words"; + + })(); +`; + +const updateSavingState = (value) => ` + (() => { + info = document.querySelector(infoBar); + info.querySelector('#infosaved').innerText = "${value}"; + })(); +`; + +export const clearEditor = ` + editor.setContent(""); +`; +const clearTitle = ` + document.getElementById(titleInput).value = ''; +`; + +const focusTitle = ` +document.getElementById("titleInput").focus(); +`; + +const setTitle = (value) => ` +document.getElementById(titleInput).value = '${value}'; +`; + +const cacheRange = `current_selection_range = editor.selection.getRng();`; +const clearRange = `current_selection_range = null`; + +const toggleFormat = (format) => { + let command = ` + tinymce.activeEditor.execCommand(${format}); + `; + + return command; +}; + +const formatWithValue = (format, value) => { + let command = ` + tinymce.activeEditor.execCommand(${format},false, '${value}'); + `; + return command; +}; + +export const nomenu = (enabled) => ` + (() => { + let isenabled = ${enabled}; + let titleIn = document.getElementById('titlebar'); + if (isenabled) { + titleIn.style['padding-left'] = 12; + titleIn.style['padding-right'] = window.innerWidth * 0.4; + } else { + titleIn.style['padding-left'] = 60; + titleIn.style['padding-right'] = window.innerWidth * 0.4; + } + })(); +`; +const updateTheme = (value) => ` +(() => { + let v = ${value} + pageTheme.colors = v; + setTheme(); +})(); + +`; + +const html = (value) => ` +editor.setContent("${value}"); +info.querySelector('#infowords').innerText = +editor.plugins.wordcount.getCount() + " words"; +`; + +const focusEditor = ` + editor.focus(); +`; + +function call(webview, func) { + webview.current?.injectJavaScript(func); +} + +const undo = ` + +editor.undoManager.undo(); + +`; +const redo = ` + +editor.undoManager.redo(); + +`; + +const clearHistory = ` +editor.undoManager.clear(); +`; + +export default { + undo, + redo, + clearHistory, + call, + focusEditor, + html, + toggleFormat, + formatWithValue, + cacheRange, + clearRange, + blur, + reset, + clearTitle, + focusTitle, + updateDateEdited, + updateSavingState, + updateTheme, + setTitle, +}; diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/colorgroup.js b/apps/mobile/src/views/Editor/tiny/toolbar/colorgroup.js new file mode 100644 index 000000000..75ca126bc --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/colorgroup.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { ScrollView } from 'react-native'; +import ColorItem from './coloritem'; + +const ColorGroup = ({group}) => { + return ( + + {group.data.map((item) => ( + + ))} + + ); + }; + + + export default ColorGroup; + \ No newline at end of file diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/coloritem.js b/apps/mobile/src/views/Editor/tiny/toolbar/coloritem.js new file mode 100644 index 000000000..b786b7f1f --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/coloritem.js @@ -0,0 +1,86 @@ +import React, { useEffect, useState } from 'react'; +import { View } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useTracked } from '../../../../provider'; +import { eSubscribeEvent, eUnSubscribeEvent } from '../../../../services/EventManager'; +import { SIZE } from '../../../../utils/SizeUtils'; +import { formatSelection, properties, rgbToHex } from './constants'; +import { execCommands } from './commands'; + +const ColorItem = ({value, format}) => { + const [state] = useTracked(); + const {colors} = state; + const [selected, setSelected] = useState(false); + + useEffect(() => { + eSubscribeEvent('onSelectionChange', onSelectionChange); + return () => { + eUnSubscribeEvent('onSelectionChange', onSelectionChange); + }; + }, [selected]); + + useEffect(() => { + onSelectionChange(properties.selection); + }, []); + + const onSelectionChange = (data) => { + if (properties.pauseSelectionChange) return; + checkForChanges(data); + }; + + const checkForChanges = (data) => { + properties.selection = data; + let formats = Object.keys(data); + let _color; + if (data[format] !== null) { + if (data[format]?.startsWith('#')) { + _color = data[format]; + } else { + _color = rgbToHex(data[format]); + } + } + if (formats.indexOf(format) > -1 && _color === format) { + setSelected(true); + return; + } + setSelected(false); + }; + + const onPress = () => { + if (selected) { + formatSelection(type, null); + } else { + formatSelection(execCommands[type](value)); + } + }; + + return ( + + + {selected && } + + + ); + }; + + export default ColorItem; \ No newline at end of file diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/commands.js b/apps/mobile/src/views/Editor/tiny/toolbar/commands.js new file mode 100644 index 000000000..232bfbff4 --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/commands.js @@ -0,0 +1,75 @@ +import DocumentPicker from 'react-native-document-picker'; +import RNFetchBlob from 'rn-fetch-blob'; +import {formatSelection} from './constants'; + +export const execCommands = { + bold: `tinymce.activeEditor.execCommand('Bold');`, + alignleft: `tinymce.activeEditor.execCommand('JustifyLeft');`, + alignright: `tinymce.activeEditor.execCommand('JustifyRight');`, + aligncenter: `tinymce.activeEditor.execCommand('JustifyCenter');`, + alignjustify: `tinymce.activeEditor.execCommand('JustifyFull');`, + italic: `tinymce.activeEditor.execCommand('Italic');`, + strikethrough: `tinymce.activeEditor.execCommand('Strikethrough');`, + underline: `tinymce.activeEditor.execCommand('Underline');`, + superscript: `tinymce.activeEditor.execCommand('Superscript');`, + subscript: `tinymce.activeEditor.execCommand('Subscript');`, + hilitecolor: (color) => + `tinymce.activeEditor.execCommand('ForeColor',false, '${color}');`, + forecolor: (color) => + `tinymce.activeEditor.execCommand('HiliteColor',false, '${color}');`, + + fontname: (fontname) => + `tinymce.activeEditor.execCommand('FontName',false, '${fontname}');`, + + indent: `tinymce.activeEditor.execCommand('Indent');`, + outdent: `tinymce.activeEditor.execCommand('Outdent');`, + blockquote: `tinymce.activeEditor.execCommand('mceBlockQuote');`, + link: (link) => + `tinymce.activeEditor.execCommand('mceInsertLink',false, '${link}');`, + unlink: `tinymce.activeEditor.execCommand('Unlink')`, + fontsize: (size) => + `tinymce.activeEditor.execCommand('FontSize', false, '${size}');`, + removeformat: `tinymce.activeEditor.execCommand('RemoveFormat');`, + p: `tinymce.activeEditor.execCommand('FormatBlock', false, 'p');`, + h2: `tinymce.activeEditor.execCommand('FormatBlock', false, 'h2');`, + h3: `tinymce.activeEditor.execCommand('FormatBlock', false, 'h3');`, + h4: `tinymce.activeEditor.execCommand('FormatBlock', false, 'h4');`, + h5: `tinymce.activeEditor.execCommand('FormatBlock', false, 'h5');`, + h6: `tinymce.activeEditor.execCommand('FormatBlock', false, 'h6');`, + pre: `tinymce.activeEditor.execCommand('FormatBlock', false, 'pre');`, + ol: (style) => + `tinymce.activeEditor.execCommand('InsertOrderedList', false, {'list-style-type': "${style}"});`, + ul: (style) => + `tinymce.activeEditor.execCommand('InsertUnorderedList', false, {'list-style-type': "${style}"});`, + removeList: `tinymce.activeEditor.execCommand('RemoveList');`, + horizontal: `tinymce.activeEditor.execCommand('InsertHorizontalRule');`, + rtl: `tinymce.activeEditor.execCommand('mceDirectionRTL');`, + ltr: `tinymce.activeEditor.execCommand('mceDirectionLTR');`, + table: (r, c) => + `tinymce.activeEditor.execCommand('mceInsertTable', false, { rows: ${r}, columns: ${c} })`, + cl: `tinymce.activeEditor.execCommand('InsertCheckList')`, + image: () => { + DocumentPicker.pick({ + type: [DocumentPicker.types.images], + }).then((r) => { + RNFetchBlob.fs.readFile(r.uri, 'base64').then((read) => { + let b64 = `data:${r.type};base64, ` + read; + formatSelection(` + minifyImg( + "${b64}", + 600, + 'image/jpeg', + (r) => { + var content = ""; + editor.undoManager.transact(() => editor.execCommand("mceInsertContent",false,content)); + }, + 0.7, + ); + `); + }); + }); + }, + video: `tinymce.activeEditor.execCommand('mceMedia')`, + /* +,*/ +}; diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/config.js b/apps/mobile/src/views/Editor/tiny/toolbar/config.js new file mode 100644 index 000000000..128d0bcf8 --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/config.js @@ -0,0 +1,308 @@ +import { + editor_font_size, + font_names, + orderedListStyles, + unorderedListStyles, + editor_colors, +} from './constants'; + +export const TOOLBAR_CONFIG = [ + [ + { + format: 'bold', + type: 'format', + fullname: 'Bold', + }, + { + format: 'italic', + type: 'format', + fullname: 'Italic', + }, + { + format: 'strikethrough', + type: 'format', + fullname: 'Strikethrough', + }, + { + format: 'underline', + type: 'format', + fullname: 'Underline', + }, + ], + + [ + { + format: 'align', + type: 'tooltip', + valueIcon: 'alignleft', + fullname: 'Text Alignment', + group: [ + { + format: 'alignleft', + type: 'formatLine', + premium: true, + fullname: 'Align Left', + }, + { + format: 'aligncenter', + type: 'formatLine', + premium: true, + fullname: 'Align Center', + }, + { + format: 'alignright', + type: 'formatLine', + premium: true, + fullname: 'Align Right', + }, + { + format: 'alignjustify', + type: 'formatLine', + premium: true, + fullname: 'Justify', + }, + ], + }, + ], + [ + { + format: 'ol', + type: 'tooltip', + fullname: 'Ordered List', + group: orderedListStyles.map((style) => { + return { + format: 'ol', + formatValue: style, + fullname: style, + premium: true, + }; + }), + }, + + { + format: 'ul', + type: 'tooltip', + fullname: 'Unordered List', + group: unorderedListStyles.map((style) => { + return { + format: 'ul', + formatValue: style === 'default' ? 'disc' : style, + fullname: style, + premium: true, + }; + }), + }, + { + format: 'cl', + type: 'format', + fullname: 'Checklist', + premium: true, + }, + ], + [ + { + format: 'fontsize', + type: 'tooltip', + valueIcon: 'fontsize', + fullname: 'Font Size', + premium: true, + group: editor_font_size.map((item) => { + return { + format: 'fontsize', + formatValue: item, + fullname: item, + }; + }), + }, + { + format: 'fontname', + type: 'tooltip', + textValue: 'System Font', + fullname: 'Font Family', + premium: true, + group: font_names.map((item) => { + return { + format: 'fontname', + formatValue: item.value, + fullname: item.value, + text: item.name, + }; + }), + }, + { + format: 'hilitecolor', + type: 'tooltip', + fullname: 'Text Highlight Color', + groupType: 'hilitecolor', + group: editor_colors.map((item) => { + return { + format: 'hilitecolor', + formatValue: item, + premium: true, + fullname: item, + }; + }), + }, + { + format: 'forecolor', + type: 'tooltip', + fullname: 'Text Color', + groupType: 'forecolor', + group: editor_colors.map((item) => { + return { + format: 'forecolor', + formatValue: item, + fullname: item, + premium: true, + }; + }), + }, + ], + [ + { + format: 'header', + type: 'tooltip', + valueIcon: 'header', + textValue: 'Paragraph', + fullname: 'Block', + group: [ + { + format: 'p', + text: 'Paragraph', + type: 'format', + showTitle: false, + premium: true, + fullname: 'Paragraph', + }, + { + format: 'h2', + text: 'Heading 2', + type: 'format', + showTitle: false, + premium: true, + fullname: 'Heading 2', + }, + { + format: 'h3', + text: 'Heading 3', + type: 'format', + showTitle: false, + premium: true, + fullname: 'Heading 3', + }, + { + format: 'h4', + text: 'Heading 4', + type: 'format', + showTitle: false, + premium: true, + fullname: 'Heading 4', + }, + { + format: 'h5', + text: 'Heading 5', + type: 'format', + showTitle: false, + premium: true, + fullname: 'Heading 5', + }, + { + format: 'h6', + text: 'Heading 6', + type: 'format', + showTitle: false, + premium: true, + fullname: 'Heading 6', + }, + ], + }, + ], + + [ + { + format: 'blockquote', + type: 'format', + fullname: 'Quote', + }, + { + format: 'link', + type: 'tooltip', + groupType: 'link', + fullname: 'Link', + premium: true, + group: [ + { + format: 'link', + type: 'format', + fullname: 'Link', + }, + ], + }, + { + format: 'code-block', + type: 'formatLine', + fullname: 'Code', + premium: true, + }, + ], + [ + { + format: 'image', + type: 'format', + fullname: 'Image', + premium: true, + }, + { + format: 'video', + type: 'format', + groupType: 'video', + fullname: 'Video', + premium: true, + }, + ], + [ + { + format: 'superscript', + type: 'format', + premium: true, + fullname: 'Superscript', + }, + { + format: 'subscript', + type: 'format', + fullname: 'Subscript', + premium: true, + }, + { + format: 'table', + type: 'tooltip', + fullname: 'Table', + groupType: 'table', + }, + ], + [ + { + format: 'rtl', + type: 'format', + fullname: 'Text Direction RTL', + }, + { + format: 'ltr', + type: 'format', + fullname: 'Text Direction LTR', + }, + ], + [ + { + format: 'horizontal', + type: 'format', + fullname: 'Horizontal Rule', + }, + + { + format: 'removeformat', + type: 'format', + fullname: 'Remove Formatting', + }, + ], +]; diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/constants.js b/apps/mobile/src/views/Editor/tiny/toolbar/constants.js new file mode 100644 index 000000000..20b925eaf --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/constants.js @@ -0,0 +1,156 @@ +import { Platform } from "react-native"; +import { EditorWebView } from "../../Functions"; +import tiny from "../tiny"; + +export const properties = { + selection: {}, + pauseSelectionChange: false, + inputMode: 1, + userBlur: false, +}; + +export function formatSelection(command) { + EditorWebView.current?.injectJavaScript(command); +} + +export function focusEditor(format) { + Platform.OS === 'android' && EditorWebView.current.requestFocus(); + if (format === 'link' || format === 'video') { + tiny.call(EditorWebView, tiny.blur + ' ' + tiny.focusEditor); + } else { + tiny.call(EditorWebView, tiny.focusEditor); + } +} + +export function rgbToHex(color) { + color = '' + color; + if (!color || color.indexOf('rgb') < 0) { + return; + } + + if (color.charAt(0) == '#') { + return color; + } + + var nums = /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/i.exec(color), + r = parseInt(nums[2], 10).toString(16), + g = parseInt(nums[3], 10).toString(16), + b = parseInt(nums[4], 10).toString(16); + + return ( + '#' + + ((r.length == 1 ? '0' + r : r) + + (g.length == 1 ? '0' + g : g) + + (b.length == 1 ? '0' + b : b)) + ); +} + +export const INPUT_MODE = { + EDITING: 1, + NO_EDIT: 2, +}; + +export const font_names = [ + {name: 'System Font', value: ''}, + {name: 'Times New Roman', value: 'times new roman'}, + {name: 'Serif', value: 'serif'}, + {name: 'Sans', value: 'sans-serif'}, + {name: 'Mono', value: 'monospace'}, + {name: 'Classic', value: 'courier new'}, +]; + +export const editor_colors = [ + '#000000', + '#e60000', + '#ff9900', + '#ffff00', + '#008a00', + '#0066cc', + '#9933ff', + '#ffffff', + '#facccc', + '#ffebcc', + '#ffffcc', + '#cce8cc', + '#cce0f5', + '#ebd6ff', + '#bbbbbb', + '#f06666', + '#ffc266', + '#ffff66', + '#66b966', + '#66a3e0', + '#c285ff', + '#888888', + '#a10000', + '#b26b00', + '#b2b200', + '#006100', + '#0047b2', + '#6b24b2', + '#444444', + '#5c0000', + '#663d00', + '#666600', + '#003700', + '#002966', + '#3d1466', +]; + +export const editor_font_size = [ + '8pt', + '10pt', + '12pt', + '14pt', + '18pt', + '24pt', + '36pt', +]; + +export const unorderedListStyles = ['default', 'circle', 'square']; + +export const orderedListStyles = [ + 'default', + 'lower-alpha', + 'lower-greek', + 'lower-roman', + 'upper-alpha', + 'upper-roman', +]; + +export const TOOLBAR_ICONS = { + bold: 'format-bold', + alignleft: 'format-align-left', + alignright: 'format-align-right', + aligncenter: 'format-align-center', + alignjustify: 'format-align-justify', + italic: 'format-italic', + strikethrough: 'format-strikethrough', + underline: 'format-underline', + superscript: 'format-superscript', + subscript: 'format-subscript', + blockquote: 'format-quote-open', + indent: 'format-indent-increase', + outdent: 'format-indent-decrease', + rtl: 'format-textdirection-r-to-l', + ltr: 'format-textdirection-l-to-r', + link: 'link', + 'code-block': 'code-braces', + image: 'image', + video: 'video', + ul: 'format-list-bulleted', + ol: 'format-list-numbered', + cl: 'format-list-checkbox', + hilitecolor: 'format-color-highlight', + forecolor: 'format-color-text', + p: 'format-paragraph', + h2: 'format-header-2', + h3: 'format-header-3', + h4: 'format-header-4', + h5: 'format-header-5', + h6: 'format-header-6', + fontsize: 'format-font-size-increase', + removeformat: 'format-clear', + horizontal:"border-horizontal", + table:'table-plus' +}; diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/group.js b/apps/mobile/src/views/Editor/tiny/toolbar/group.js new file mode 100644 index 000000000..54ec67aaf --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/group.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { View } from 'react-native'; +import ToolbarItem from './item'; + +const ToolbarGroup = ({group}) => { + return ( + + {group.map((item) => ( + + ))} + + ); + }; + + export default ToolbarGroup; \ No newline at end of file diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/index.js b/apps/mobile/src/views/Editor/tiny/toolbar/index.js new file mode 100644 index 000000000..a981f9e3d --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/index.js @@ -0,0 +1,55 @@ +import React, {useEffect} from 'react'; +import {View} from 'react-native'; +import {ScrollView} from 'react-native'; +import {useTracked} from '../../../../provider'; +import {eSendEvent} from '../../../../services/EventManager'; +import {TOOLBAR_CONFIG} from './config'; +import {properties} from './constants'; +import ToolbarGroup from './group'; +import Tooltip from './tooltip'; + +const EditorToolbar = React.memo( + () => { + const [state] = useTracked(); + const {colors} = state; + const config = TOOLBAR_CONFIG; + + useEffect(() => { + properties.selection = {}; + return () => { + properties.selection = {}; + }; + }, []); + + return ( + <> + + { + eSendEvent('showTooltip'); + }} + keyboardShouldPersistTaps="always" + keyboardDismissMode="none" + horizontal={true} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + bounces={false}> + {config.map((item, index) => ( + + ))} + + + ); + }, + () => true, +); + +export default EditorToolbar; diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/item.js b/apps/mobile/src/views/Editor/tiny/toolbar/item.js new file mode 100644 index 000000000..004b105b3 --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/item.js @@ -0,0 +1,307 @@ +import React, {useEffect, useState} from 'react'; +import {View} from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import {PressableButton} from '../../../../components/PressableButton'; +import Paragraph from '../../../../components/Typography/Paragraph'; +import {useTracked} from '../../../../provider'; +import { + eSubscribeEvent, + eUnSubscribeEvent, + eSendEvent, +} from '../../../../services/EventManager'; +import {editing, showTooltip, TOOLTIP_POSITIONS} from '../../../../utils'; +import {SIZE} from '../../../../utils/SizeUtils'; +import {execCommands} from './commands'; +import { + focusEditor, + formatSelection, + properties, + TOOLBAR_ICONS, + rgbToHex, + font_names, +} from './constants'; +import ToolbarItemPin from './itempin'; +import ToolbarListFormat from './listformat'; + +const ToolbarItem = ({ + format, + type, + showTitle, + premium, + valueIcon, + group, + groupFormat, + groupDefault, + value, + groupType, + text, + formatValue, + fullname +}) => { + const [state] = useTracked(); + const {colors} = state; + const [selected, setSelected] = useState(false); + const [icon, setIcon] = useState(valueIcon); + const [color, setColor] = useState(null); + const [currentText, setCurrentText] = useState(null); + + useEffect(() => { + eSubscribeEvent('onSelectionChange', onSelectionChange); + return () => { + eUnSubscribeEvent('onSelectionChange', onSelectionChange); + }; + }, []); + + useEffect(() => { + onSelectionChange(properties.selection); + }, []); + + const checkForChanges = (data) => { + properties.selection = data; + let formats = Object.keys(data); + if (format === 'header' && type === 'tooltip') { + let keys = group.map((i) => i.format); + keys.forEach((k) => { + if (formats.includes(k)) { + setCurrentText(group.find((e) => e.format === k).text); + setSelected(false); + } + }); + return; + } + if (formats.indexOf(format) > -1 && format !== 'removeformat') { + if (format === 'forecolor' || format === 'hilitecolor') { + if (!data[format]) { + setSelected(false); + return; + } + if (data[format]?.startsWith('#')) { + setColor(data[format]); + } else { + setColor(rgbToHex(data[format])); + } + } + + if ( + format === 'fontname' && + (data[format].includes('courier') || + data[format].includes('times new roman')) + ) { + data[format] = JSON.parse(data[format]); + } + + if ( + (format === 'fontsize' || + format === 'ul' || + format === 'ol' || + format === 'fontname') && + data[format] !== '' && + formatValue === data[format] + ) { + setSelected(true); + return; + } + + if (format === 'fontname' && formatValue === data[format]) { + setSelected(true); + return; + } + + if ((format === 'ul' || format === 'ol') && type === 'tooltip') { + setIcon(data[format]); + setSelected(true); + return; + } + + if (format === 'fontname' && type === 'tooltip') { + font_names.forEach((font) => { + if (font.value === data[format]) { + setCurrentText(font.name); + setIcon(font.value); + } + }); + setSelected(false); + return; + } + + if (format === 'fontsize' && type === 'tooltip') { + setCurrentText(data[format]); + setSelected(false); + return; + } + if ( + format === 'fontsize' || + format === 'fontname' || + format === 'ol' || + format === 'ul' + ) { + setSelected(false); + return; + } + setSelected(true); + return; + } + + setSelected(false); + + if (valueIcon && group) { + setIcon(valueIcon); + } + if (format === 'forecolor' || format === 'hilitecolor') { + setColor(null); + } + }; + + const onSelectionChange = (data) => { + if (properties.pauseSelectionChange) return; + + checkForChanges(data); + }; + + const onPress = async (event) => { + /* if (premium && !PremiumService.get()) { + + let user = await db.user.getUser(); + if (user && !user.isEmailConfirmed) { + await sleep(500); + PremiumService.showVerifyEmailDialog(); + } else { + eSendEvent(eShowGetPremium, { + context: 'editor', + title: 'Get Notesnook Pro', + desc: 'Enjoy Full Rich Text Editor with Markdown Support!', + }); + } + return; + } */ + if (editing.tooltip === format && !formatValue) { + focusEditor(format); + eSendEvent('showTooltip'); + editing.tooltip = null; + properties.pauseSelectionChange = false; + return; + } + + if ( + (format === 'link' && properties.selection.current?.length === 0) || + !properties.selection + ) { + return; + } + + if (format === 'link' || format === 'video') { + pauseSelectionChange = true; + formatSelection(`current_selection_range = editor.selection.getRng();`); + } + + if (type === 'tooltip') { + editing.tooltip = format; + eSendEvent('showTooltip', { + data: group, + title: format, + default: valueIcon, + type: groupType, + pageX: event.nativeEvent.pageX, + }); + return; + } + + if (format === "image") { + execCommands.image(); + return; + } + + let value; + if ( + groupFormat === 'fontsize' || + groupFormat === 'fontname' || + groupFormat === 'ol' || + groupFormat === 'ul' + ) { + value = formatValue; + } + + if (selected && (format === 'ol' || format === 'ul' || format === 'cl')) { + formatSelection(execCommands.removeList); + } else if (value) { + formatSelection(execCommands[format](value)); + } else { + formatSelection(execCommands[format]); + } + + focusEditor(format); + editing.tooltip = null; + }; + + return ( + + { + showTooltip(event, fullname, TOOLTIP_POSITIONS.TOP); + }} + onPress={(e) => onPress(e)} + customStyle={{ + borderRadius: 0, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + height: 40, + minWidth: 40, + borderRadius: 5, + marginTop: 5, + marginBottom: 5, + marginHorizontal: 4, + }}> + {type === 'tooltip' && ( + + )} + {format === 'ol' || format === 'ul' || format === 'cl' ? ( + + ) : format === 'fontsize' ? ( + + {formatValue || currentText} + + ) : text && groupFormat !== 'header' ? ( + + {currentText || text} + + ) : ( + + )} + + + ); +}; + +export default ToolbarItem; diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/itempin.js b/apps/mobile/src/views/Editor/tiny/toolbar/itempin.js new file mode 100644 index 000000000..53e6b6ad5 --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/itempin.js @@ -0,0 +1,44 @@ +import React, { useEffect, useState } from 'react'; +import { View } from 'react-native'; +import { eSubscribeEvent, eUnSubscribeEvent } from '../../../../services/EventManager'; +import { useTracked } from '../../../../provider'; + + +const ToolbarItemPin = ({format, color}) => { + const [state] = useTracked(); + const {colors} = state; + const [visible, setVisible] = useState(false); + + useEffect(() => { + eSubscribeEvent('showTooltip', show); + return () => { + eUnSubscribeEvent('showTooltip', show); + }; + }, []); + + const show = (data) => { + if (data?.title === format) { + setVisible(true); + } else { + setVisible(false); + } + }; + + return ( + visible && ( + + ) + ); + }; + + export default ToolbarItemPin \ No newline at end of file diff --git a/apps/mobile/src/views/Editor/tiny/toolbar/linkinput.js b/apps/mobile/src/views/Editor/tiny/toolbar/linkinput.js new file mode 100644 index 000000000..433be19d5 --- /dev/null +++ b/apps/mobile/src/views/Editor/tiny/toolbar/linkinput.js @@ -0,0 +1,121 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {Linking, View} from 'react-native'; +import {eSendEvent} from '../../../../services/EventManager'; +import {Button} from '../../../../components/Button'; +import Input from '../../../../components/Input'; +import {useTracked} from '../../../../provider'; +import {editing} from '../../../../utils'; +import {execCommands} from './commands'; +import {formatSelection, INPUT_MODE, properties} from './constants'; +import LinkPreview from './linkpreview'; + +let inputValue = null; + +const ToolbarLinkInput = ({format, value, setVisible}) => { + const [state] = useTracked(); + const {colors} = state; + const [mode, setMode] = useState( + value ? INPUT_MODE.NO_EDIT : INPUT_MODE.EDITING, + ); + + const inputRef = useRef(); + + useEffect(() => { + if (!value) { + inputRef.current?.focus(); + formatSelection(`editor.selection.setRng(current_selection_range);`); + } + properties.inputMode = value ? INPUT_MODE.NO_EDIT : INPUT_MODE.EDITING; + editing.tooltip = format; + properties.userBlur = false; + if (properties.pauseSelectionChange) { + setTimeout(() => { + properties.pauseSelectionChange = false; + }, 100); + } + return () => { + properties.inputMode = null; + editing.tooltip = null; + inputValue = null; + }; + }, [format]); + + const onChangeText = (value) => { + inputValue = value; + }; + + const onSubmit = (value) => { + if (value === 'clear') { + inputValue = null; + } + properties.userBlur = true; + if (inputValue === '' || !inputValue) { + formatSelection(execCommands.unlink); + } else { + formatSelection(execCommands[format](inputValue)); + } + formatSelection( + `editor.selection.setRng(current_selection_range); current_selection_range = null`, + ); + setVisible(false); + editing.tooltip = null; + focusEditor(format); + properties.userBlur = false; + }; + + const onPress = async () => { + await Linking.openURL(value); + }; + + const onBlur = async () => { + formatSelection(`current_selection_range = null`); + if (properties.userBlur) return; + focusEditor('custom'); + eSendEvent('showTooltip'); + }; + + return ( + + {mode === INPUT_MODE.NO_EDIT ? ( + + ) : ( + <> + + + {mode === INPUT_MODE.EDITING && ( +