add custom toolbar for editor

This commit is contained in:
ammarahm-ed
2021-02-04 14:34:14 +05:00
parent 2a1a5d737d
commit ecd6acdbe4
20 changed files with 2009 additions and 132 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -70,7 +70,6 @@ export const EditorTitle = () => {
placeholder="Title Here"
onChangeText={(text) => {
setTitle(text);
post('title', text);
_onMessage({
nativeEvent: {
data: JSON.stringify({

View File

@@ -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);
};

View File

@@ -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;

View File

@@ -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 />
</>
);
},

View 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,
};

View 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;

View 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;

View 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')`,
/*
,*/
};

View 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',
},
],
];

View 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'
};

View 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;

View 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;

View 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;

View 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

View 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;

View 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;

View 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;

View 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;