mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-03 04:31:46 +02:00
add custom toolbar for editor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@ export const EditorTitle = () => {
|
||||
placeholder="Title Here"
|
||||
onChangeText={(text) => {
|
||||
setTitle(text);
|
||||
post('title', text);
|
||||
_onMessage({
|
||||
nativeEvent: {
|
||||
data: JSON.stringify({
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
<ActionIcon
|
||||
name="undo"
|
||||
disabled={historyState.undo === 0}
|
||||
color={colors.heading}
|
||||
customStyle={{
|
||||
marginLeft: 10,
|
||||
}}
|
||||
onPress={() => {
|
||||
if (historyState.undo === 0) return;
|
||||
post('undo');
|
||||
}}
|
||||
/>
|
||||
<ActionIcon
|
||||
name="redo"
|
||||
disabled={historyState.redo=== 0}
|
||||
color={colors.heading}
|
||||
customStyle={{
|
||||
marginLeft: 10,
|
||||
}}
|
||||
onPress={() => {
|
||||
if (historyState.redo === 0) return;
|
||||
post('redo');
|
||||
}}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
name="undo"
|
||||
disabled={!historyState.undo}
|
||||
color={colors.heading}
|
||||
customStyle={{
|
||||
marginLeft: 10,
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!historyState.undo) return;
|
||||
tiny.call(EditorWebView, tiny.undo);
|
||||
}}
|
||||
/>
|
||||
<ActionIcon
|
||||
name="redo"
|
||||
disabled={!historyState.redo}
|
||||
color={colors.heading}
|
||||
customStyle={{
|
||||
marginLeft: 10,
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!historyState.redo) return;
|
||||
tiny.call(EditorWebView, tiny.redo);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HistoryComponent;
|
||||
|
||||
@@ -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 ? (
|
||||
<Loading
|
||||
@@ -78,34 +82,55 @@ const Editor = React.memo(
|
||||
style={{height: 1, padding: 0, width: 1, position: 'absolute'}}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<EditorHeader />
|
||||
<WebView
|
||||
testID={notesnook.ids.default.editor}
|
||||
ref={EditorWebView}
|
||||
onLoad={onLoad}
|
||||
onError={(event) => {
|
||||
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}
|
||||
<ScrollView
|
||||
bounces={false}
|
||||
allowFileAccess={true}
|
||||
scalesPageToFit={true}
|
||||
allowingReadAccessToURL={Platform.OS === 'android' ? true : null}
|
||||
allowFileAccessFromFileURLs={true}
|
||||
allowUniversalAccessFromFileURLs={true}
|
||||
originWhitelist={['*']}
|
||||
source={source}
|
||||
style={style}
|
||||
onMessage={_onMessage}
|
||||
/>
|
||||
bouncesZoom={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
contentContainerStyle={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}>
|
||||
<EditorHeader />
|
||||
<WebView
|
||||
testID={notesnook.ids.default.editor}
|
||||
ref={EditorWebView}
|
||||
onLoad={onLoad}
|
||||
onError={(event) => {
|
||||
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}
|
||||
/>
|
||||
</ScrollView>
|
||||
<EditorToolbar />
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
153
apps/mobile/src/views/Editor/tiny/tiny.js
Normal file
153
apps/mobile/src/views/Editor/tiny/tiny.js
Normal file
@@ -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,
|
||||
};
|
||||
33
apps/mobile/src/views/Editor/tiny/toolbar/colorgroup.js
Normal file
33
apps/mobile/src/views/Editor/tiny/toolbar/colorgroup.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import ColorItem from './coloritem';
|
||||
|
||||
const ColorGroup = ({group}) => {
|
||||
return (
|
||||
<ScrollView
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
}}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
bounces={false}
|
||||
horizontal={true}
|
||||
contentContainerStyle={{
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{group.data.map((item) => (
|
||||
<ColorItem
|
||||
key={item.formatValue}
|
||||
value={item.formatValue}
|
||||
format={group.format}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default ColorGroup;
|
||||
|
||||
86
apps/mobile/src/views/Editor/tiny/toolbar/coloritem.js
Normal file
86
apps/mobile/src/views/Editor/tiny/toolbar/coloritem.js
Normal file
@@ -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 (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
backgroundColor: value,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.nav,
|
||||
borderRadius: 5,
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 5,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
borderRadius: 5,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: selected ? 'rgba(0,0,0,0.1)' : 'transparent',
|
||||
}}>
|
||||
{selected && <Icon name="check" size={SIZE.lg} color="white" />}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColorItem;
|
||||
75
apps/mobile/src/views/Editor/tiny/toolbar/commands.js
Normal file
75
apps/mobile/src/views/Editor/tiny/toolbar/commands.js
Normal file
@@ -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 = "<img style=" + "max-width:100% !important;" + "src=" + r + ">";
|
||||
editor.undoManager.transact(() => editor.execCommand("mceInsertContent",false,content));
|
||||
},
|
||||
0.7,
|
||||
);
|
||||
`);
|
||||
});
|
||||
});
|
||||
},
|
||||
video: `tinymce.activeEditor.execCommand('mceMedia')`,
|
||||
/*
|
||||
,*/
|
||||
};
|
||||
308
apps/mobile/src/views/Editor/tiny/toolbar/config.js
Normal file
308
apps/mobile/src/views/Editor/tiny/toolbar/config.js
Normal file
@@ -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',
|
||||
},
|
||||
],
|
||||
];
|
||||
156
apps/mobile/src/views/Editor/tiny/toolbar/constants.js
Normal file
156
apps/mobile/src/views/Editor/tiny/toolbar/constants.js
Normal file
@@ -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'
|
||||
};
|
||||
33
apps/mobile/src/views/Editor/tiny/toolbar/group.js
Normal file
33
apps/mobile/src/views/Editor/tiny/toolbar/group.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import ToolbarItem from './item';
|
||||
|
||||
const ToolbarGroup = ({group}) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingRight: 12,
|
||||
}}>
|
||||
{group.map((item) => (
|
||||
<ToolbarItem
|
||||
key={item.format}
|
||||
format={item.format}
|
||||
formatValue={item.formatValue}
|
||||
type={item.type}
|
||||
showTitle={item.showTitle}
|
||||
premium={item.premium}
|
||||
valueIcon={item.valueIcon}
|
||||
group={item.group}
|
||||
groupType={item.groupType}
|
||||
text={item.textValue || item.text}
|
||||
fullname={item.fullname}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarGroup;
|
||||
55
apps/mobile/src/views/Editor/tiny/toolbar/index.js
Normal file
55
apps/mobile/src/views/Editor/tiny/toolbar/index.js
Normal file
@@ -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 (
|
||||
<>
|
||||
<Tooltip />
|
||||
<ScrollView
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
minHeight: 50,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.nav,
|
||||
paddingLeft: 12,
|
||||
}}
|
||||
onScrollBeginDrag={() => {
|
||||
eSendEvent('showTooltip');
|
||||
}}
|
||||
keyboardShouldPersistTaps="always"
|
||||
keyboardDismissMode="none"
|
||||
horizontal={true}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
bounces={false}>
|
||||
{config.map((item, index) => (
|
||||
<ToolbarGroup key={item[0].format} group={item} />
|
||||
))}
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
},
|
||||
() => true,
|
||||
);
|
||||
|
||||
export default EditorToolbar;
|
||||
307
apps/mobile/src/views/Editor/tiny/toolbar/item.js
Normal file
307
apps/mobile/src/views/Editor/tiny/toolbar/item.js
Normal file
@@ -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 (
|
||||
<View>
|
||||
<PressableButton
|
||||
type={selected ? 'shade' : 'transparent'}
|
||||
customColor={selected && color}
|
||||
customSelectedColor={selected && color}
|
||||
customOpacity={0.12}
|
||||
onLongPress={(event) => {
|
||||
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' && (
|
||||
<ToolbarItemPin format={format} color={selected && color} />
|
||||
)}
|
||||
{format === 'ol' || format === 'ul' || format === 'cl' ? (
|
||||
<ToolbarListFormat
|
||||
format={format}
|
||||
selected={selected}
|
||||
formatValue={formatValue || icon || 'default'}
|
||||
/>
|
||||
) : format === 'fontsize' ? (
|
||||
<Paragraph
|
||||
size={SIZE.md}
|
||||
color={selected ? colors.accent : colors.pri}>
|
||||
{formatValue || currentText}
|
||||
</Paragraph>
|
||||
) : text && groupFormat !== 'header' ? (
|
||||
<Paragraph
|
||||
color={selected ? colors.accent : colors.pri}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
fontFamily:
|
||||
format === 'fontname' && formatValue
|
||||
? formatValue
|
||||
: format === 'fontname' && icon
|
||||
? icon
|
||||
: null,
|
||||
}}
|
||||
size={SIZE.md}>
|
||||
{currentText || text}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Icon
|
||||
name={
|
||||
valueIcon
|
||||
? TOOLBAR_ICONS[icon || valueIcon]
|
||||
: TOOLBAR_ICONS[format === '' ? groupDefault : format]
|
||||
}
|
||||
size={SIZE.xl}
|
||||
allowFontScaling={false}
|
||||
color={selected ? (color ? color : colors.accent) : colors.pri}
|
||||
/>
|
||||
)}
|
||||
</PressableButton>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarItem;
|
||||
44
apps/mobile/src/views/Editor/tiny/toolbar/itempin.js
Normal file
44
apps/mobile/src/views/Editor/tiny/toolbar/itempin.js
Normal file
@@ -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 && (
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
backgroundColor: color || colors.accent,
|
||||
borderRadius: 100,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
top: -10,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarItemPin
|
||||
121
apps/mobile/src/views/Editor/tiny/toolbar/linkinput.js
Normal file
121
apps/mobile/src/views/Editor/tiny/toolbar/linkinput.js
Normal file
@@ -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 (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
paddingHorizontal: 12,
|
||||
maxWidth: '100%',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{mode === INPUT_MODE.NO_EDIT ? (
|
||||
<LinkPreview value={value} setMode={setMode} onSubmit={onSubmit} />
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={inputRef}
|
||||
onBlurInput={onBlur}
|
||||
onPress={onPress}
|
||||
height={45}
|
||||
onSubmit={onSubmit}
|
||||
onChangeText={onChangeText}
|
||||
defaultValue={value}
|
||||
blurOnSubmit={false}
|
||||
loading={mode === INPUT_MODE.NO_EDIT}
|
||||
placeholder={
|
||||
format === 'video' ? 'Enter url to video' : 'Enter link'
|
||||
}
|
||||
/>
|
||||
|
||||
{mode === INPUT_MODE.EDITING && (
|
||||
<Button
|
||||
title="Save"
|
||||
onPress={onSubmit}
|
||||
height={40}
|
||||
fontSize={12}
|
||||
style={{marginLeft: 10}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarLinkInput;
|
||||
161
apps/mobile/src/views/Editor/tiny/toolbar/linkpreview.js
Normal file
161
apps/mobile/src/views/Editor/tiny/toolbar/linkpreview.js
Normal file
@@ -0,0 +1,161 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {getLinkPreview} from 'link-preview-js';
|
||||
import {Image, Linking, View} from 'react-native';
|
||||
import {TouchableOpacity} from 'react-native-gesture-handler';
|
||||
import {Button} from '../../../../components/Button';
|
||||
import Heading from '../../../../components/Typography/Heading';
|
||||
import Paragraph from '../../../../components/Typography/Paragraph';
|
||||
import {useTracked} from '../../../../provider';
|
||||
import {ToastEvent} from '../../../../services/EventManager';
|
||||
import {SIZE} from '../../../../utils/SizeUtils';
|
||||
import { INPUT_MODE, properties } from './constants';
|
||||
|
||||
let prevLink = {};
|
||||
let prevHeight = 65;
|
||||
const LinkPreview = ({setMode, value, onSubmit}) => {
|
||||
const [state] = useTracked();
|
||||
const {colors} = state;
|
||||
const [link, setLink] = useState(prevLink.value === value ? prevLink : {});
|
||||
const [height, setHeight] = useState(prevHeight);
|
||||
|
||||
useEffect(() => {
|
||||
if (value && prevLink.value !== value) {
|
||||
getLinkPreview(value)
|
||||
.then((r) => {
|
||||
if (r.contentType?.includes('text/html')) {
|
||||
prevLink = {
|
||||
value: value,
|
||||
name: r.siteName,
|
||||
title: r.title,
|
||||
description: r.description,
|
||||
image: r.images[0],
|
||||
favicon: r.favicons[0],
|
||||
};
|
||||
setLink(prevLink);
|
||||
}
|
||||
})
|
||||
.catch((e) => console.log);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderImage = (imageLink, faviconLink) => {
|
||||
return imageLink ? (
|
||||
<Image
|
||||
style={{
|
||||
width: height,
|
||||
height: height,
|
||||
borderRadius: 5,
|
||||
marginVertical: 5,
|
||||
}}
|
||||
source={{uri: imageLink}}
|
||||
/>
|
||||
) : faviconLink ? (
|
||||
<Image
|
||||
style={{
|
||||
width: height,
|
||||
height: height,
|
||||
borderRadius: 5,
|
||||
marginVertical: 5,
|
||||
}}
|
||||
source={{uri: faviconLink}}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const openLink = () => {
|
||||
Linking.openURL(value)
|
||||
.catch((e) => ToastEvent.show(e.message, 'error'))
|
||||
.then((r) => {
|
||||
console.log('closed');
|
||||
});
|
||||
};
|
||||
|
||||
const renderText = (name, title, description) => {
|
||||
return (
|
||||
<View
|
||||
onLayout={(e) => {
|
||||
prevHeight = e.nativeEvent.layout.height;
|
||||
e.nativeEvent && setHeight(prevHeight);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}>
|
||||
<TouchableOpacity onPress={openLink} activeOpacity={1}>
|
||||
{title && (
|
||||
<Heading
|
||||
numberOfLines={1}
|
||||
style={{flexWrap: 'wrap', maxWidth: '90%', paddingLeft: 5}}
|
||||
size={SIZE.sm}>
|
||||
{name ? name + ': ' + title : title}
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{description && (
|
||||
<Paragraph
|
||||
style={{flexWrap: 'wrap', maxWidth: '90%', paddingLeft: 5}}
|
||||
numberOfLines={2}
|
||||
color={colors.icon}
|
||||
size={SIZE.xs}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}>
|
||||
{!title && !description && (
|
||||
<Paragraph
|
||||
onPress={openLink}
|
||||
style={{
|
||||
textDecorationLine: 'underline',
|
||||
color: colors.accent,
|
||||
}}>
|
||||
{value}
|
||||
</Paragraph>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setMode(INPUT_MODE.EDITING);
|
||||
properties.inputMode = INPUT_MODE.EDITING;
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6,
|
||||
}}
|
||||
height={25}
|
||||
fontSize={SIZE.sm}
|
||||
title="Edit"
|
||||
/>
|
||||
<Button
|
||||
type="errorShade"
|
||||
onPress={() => {
|
||||
onSubmit('clear');
|
||||
}}
|
||||
height={25}
|
||||
fontSize={SIZE.sm}
|
||||
title="Remove"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderImage(link.image, link.favicon)}
|
||||
{renderText(link.name, link.title, link.description)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkPreview;
|
||||
81
apps/mobile/src/views/Editor/tiny/toolbar/listformat.js
Normal file
81
apps/mobile/src/views/Editor/tiny/toolbar/listformat.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import Heading from '../../../../components/Typography/Heading';
|
||||
import {useTracked} from '../../../../provider';
|
||||
|
||||
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
const ToolbarListFormat = ({selected, formatValue, format}) => {
|
||||
const [state] = useTracked();
|
||||
const {colors} = state;
|
||||
const ordered_lists = {
|
||||
default: ['1.', '2.', '3.'],
|
||||
'lower-alpha': ['a.', 'b.', 'c.'],
|
||||
'lower-greek': ['α.', 'β.', 'γ.'],
|
||||
'lower-roman': ['i.', 'ii.', 'iii.'],
|
||||
'upper-alpha': ['A.', 'B.', 'C.'],
|
||||
'upper-roman': ['I.', 'II.', 'III.'],
|
||||
};
|
||||
|
||||
const list = format === 'ol' ? ordered_lists[formatValue] : ["1", "2", "3"];
|
||||
|
||||
return (
|
||||
<View>
|
||||
{list?.map((item) => (
|
||||
<View
|
||||
key={item}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: format === 'ol' ? 0 : 2,
|
||||
marginTop: format === 'ol' || format === 'cl' ? 0 : 2,
|
||||
}}>
|
||||
{format === 'ol' ? (
|
||||
<Heading
|
||||
style={{
|
||||
width: 10,
|
||||
}}
|
||||
color={selected && colors.accent}
|
||||
size={6}>
|
||||
{item}
|
||||
</Heading>
|
||||
) : format === 'cl' ? (
|
||||
<Icon
|
||||
style={{
|
||||
width: 10,
|
||||
color: selected ? colors.accent: colors.pri,
|
||||
}}
|
||||
size={6}
|
||||
name="check-box-outline"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: formatValue === 'square' ? 0 : 100,
|
||||
backgroundColor:
|
||||
formatValue !== 'circle'
|
||||
? selected
|
||||
? colors.accent
|
||||
: colors.pri
|
||||
: null,
|
||||
borderWidth: formatValue === 'circle' ? 0.5 : 0,
|
||||
width: 4,
|
||||
height: 4,
|
||||
marginRight: 5,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 3,
|
||||
backgroundColor: selected ? colors.pri : colors.nav,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolbarListFormat;
|
||||
248
apps/mobile/src/views/Editor/tiny/toolbar/tooltip.js
Normal file
248
apps/mobile/src/views/Editor/tiny/toolbar/tooltip.js
Normal file
@@ -0,0 +1,248 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Touchable} from 'react-native';
|
||||
import {ScrollView, View} from 'react-native';
|
||||
import {TouchableOpacity} from 'react-native-gesture-handler';
|
||||
import Paragraph from '../../../../components/Typography/Paragraph';
|
||||
import {useTracked} from '../../../../provider';
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
eSendEvent,
|
||||
} from '../../../../services/EventManager';
|
||||
import {editing, getElevation} from '../../../../utils';
|
||||
import ColorGroup from './colorgroup';
|
||||
import {execCommands} from './commands';
|
||||
import {formatSelection, properties} from './constants';
|
||||
import ToolbarItem from './item';
|
||||
import ToolbarLinkInput from './linkinput';
|
||||
|
||||
const Tooltip = () => {
|
||||
const [state] = useTracked();
|
||||
const {colors} = state;
|
||||
const [group, setGroup] = useState({
|
||||
data: [],
|
||||
title: null,
|
||||
default: null,
|
||||
type: null,
|
||||
});
|
||||
const [visible, setVisible] = useState(false);
|
||||
const floating = group.type === 'table';
|
||||
useEffect(() => {
|
||||
eSubscribeEvent('showTooltip', show);
|
||||
return () => {
|
||||
eUnSubscribeEvent('showTooltip', show);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent('onSelectionChange', onSelectionChange);
|
||||
return () => {
|
||||
eUnSubscribeEvent('onSelectionChange', onSelectionChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onSelectionChange = (data) => {
|
||||
if (properties.pauseSelectionChange) return;
|
||||
if (data['link']) {
|
||||
properties.selection = data;
|
||||
properties.pauseSelectionChange = true;
|
||||
show({
|
||||
data: null,
|
||||
value: data['link'],
|
||||
type: 'link',
|
||||
});
|
||||
} else if (data['video']) {
|
||||
properties.selection = data;
|
||||
properties.pauseSelectionChange = true;
|
||||
show({
|
||||
data: null,
|
||||
value: data['video'],
|
||||
type: 'video',
|
||||
format: 'format',
|
||||
});
|
||||
} else {
|
||||
if (editing.tooltip !== 'background' && editing.tooltip !== 'color') {
|
||||
setVisible(false);
|
||||
eSendEvent('showTooltip');
|
||||
editing.tooltip = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const show = (data) => {
|
||||
properties.userBlur = true;
|
||||
if (!data) {
|
||||
setVisible(false);
|
||||
editing.tooltip = null;
|
||||
return;
|
||||
}
|
||||
setGroup(data);
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
let ParentElement = (props) =>
|
||||
group.type === 'link' ||
|
||||
group.type === 'video' ||
|
||||
group.type === 'table' ? (
|
||||
<View
|
||||
style={{
|
||||
borderRadius: floating ? 5 : 0,
|
||||
padding: floating ? 5 : 0,
|
||||
position: 'absolute',
|
||||
bottom: floating ? 60 : 50,
|
||||
width: group.type === 'table' ? 35 * 5 + 10 : '100%',
|
||||
minHeight: 50,
|
||||
backgroundColor: colors.bg,
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.nav,
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
left: floating && group?.pageX ? group.pageX / 1.9 : null,
|
||||
...getElevation(floating ? 5 : 0),
|
||||
}}
|
||||
children={props.children}
|
||||
/>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
position: 'absolute',
|
||||
bottom: 50,
|
||||
width: '100%',
|
||||
minHeight: 50,
|
||||
backgroundColor: colors.bg,
|
||||
borderRadius: 0,
|
||||
alignSelf: 'center',
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.nav,
|
||||
}}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
bounces={false}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
contentContainerStyle={{
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
minWidth: '100%',
|
||||
}}
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
visible && (
|
||||
<ParentElement>
|
||||
{!group.data && (group.type === 'link' || group.type === 'video') && (
|
||||
<ToolbarLinkInput
|
||||
format={group.type}
|
||||
setVisible={setVisible}
|
||||
type={group.type}
|
||||
value={group.value}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!group.data && group.type === 'table' && (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
].map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.toString()}
|
||||
onPress={() => {
|
||||
let columnCount = 5;
|
||||
let rowNumber = Math.floor(index / columnCount);
|
||||
let columnNumber = index - rowNumber * columnCount;
|
||||
formatSelection(
|
||||
execCommands.table(rowNumber + 1, columnNumber + 1),
|
||||
);
|
||||
eSendEvent('showTooltip');
|
||||
}}
|
||||
style={{
|
||||
width: 25,
|
||||
height: 25,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.icon,
|
||||
marginHorizontal: 5,
|
||||
marginVertical: 5,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{group.type === 'hilitecolor' || group.type === 'forecolor' ? (
|
||||
<ColorGroup group={group} />
|
||||
) : null}
|
||||
|
||||
{group.data &&
|
||||
group.type !== 'hilitecolor' &&
|
||||
group.type !== 'forecolor' &&
|
||||
group.data.map((item) =>
|
||||
group.type === 'link' || group.type === 'video' ? (
|
||||
<ToolbarLinkInput
|
||||
key={item.formatValue || item.format}
|
||||
setVisible={setVisible}
|
||||
format={item.format}
|
||||
type={item.type}
|
||||
showTitle={true}
|
||||
premium={item.premium}
|
||||
groupFormat={group.title}
|
||||
groupDefault={group.default}
|
||||
formatValue={item.formatValue}
|
||||
fullname={item.fullname}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarItem
|
||||
key={item.formatValue || item.format}
|
||||
format={item.format}
|
||||
formatValue={item.formatValue}
|
||||
type={item.type}
|
||||
showTitle={item.showTitle}
|
||||
premium={item.premium}
|
||||
groupFormat={group.title}
|
||||
groupDefault={group.default}
|
||||
text={item.text}
|
||||
fullname={item.fullname}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</ParentElement>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
Reference in New Issue
Block a user