Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Desktop: feat: Added search list for font input fields #10248

Merged
merged 27 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
56520c9
Desktop: feat: Added search list to font input fields
ab-elhaddad Apr 2, 2024
6618102
Desktop: fix: Made the font-list verion absolute
ab-elhaddad Apr 2, 2024
bef11bb
Desktop: fix: Specify font input fields
ab-elhaddad Apr 2, 2024
9832a9e
Desktop: refactor: Refactored props and added types to it
ab-elhaddad Apr 2, 2024
1362f78
Desktop: refactor: Replaced font-list with window.queryLocalFonts
ab-elhaddad Apr 3, 2024
2baed73
Desktop: refactor: Refactored component functions and used useCallback
ab-elhaddad Apr 3, 2024
f791467
Desktop: refactor: Switched dynamic styles to rscss
ab-elhaddad Apr 3, 2024
95e96e0
Desktop: refactor: Deleted `memo`
ab-elhaddad Apr 3, 2024
67800cd
Desktop: fix: used setting subType to determine font input fields
ab-elhaddad Apr 4, 2024
5113b57
Desktop: refactor: updated vars names and types
ab-elhaddad Apr 4, 2024
e86079f
Desktop: refactor: removed DOM with its dependencies
ab-elhaddad Apr 4, 2024
d9cbbe4
Desktop: refactor: removed useless code
ab-elhaddad Apr 5, 2024
32fee41
Desktop: refactor: redefined Font type
ab-elhaddad Apr 6, 2024
72ac22b
Merge branch 'dev' into font-drop-list
ab-elhaddad Apr 6, 2024
da9047a
Desktop: refactor: removed all `any`
ab-elhaddad Apr 6, 2024
d18ec92
Desktop: refactor: switched to value/onChange pattern
ab-elhaddad Apr 6, 2024
e719c97
Desktop: refactor: removed useless code
ab-elhaddad Apr 6, 2024
fde9798
Desktop: fix: make the user-facing text localisable
ab-elhaddad Apr 6, 2024
c75cf70
Desktop: fix: blur and click event conflict
ab-elhaddad Apr 6, 2024
81ba47f
Desktop: refactor: added some types
ab-elhaddad Apr 6, 2024
b38cd5d
Desktop: feat: Shows the full list even if a font is selected
ab-elhaddad Apr 7, 2024
d345bf2
Desktop: fix: added Lazy Loading to prevent the onClick freeze
ab-elhaddad Apr 8, 2024
81479d1
Desktop: fix: fonts show loading when no search results
ab-elhaddad Apr 9, 2024
c5bc4d7
Desktop: feat: Add the option (checkbox) to show only the monospaced …
ab-elhaddad Apr 11, 2024
2eb04e4
Merge branch 'font-drop-list' of remote/font-drop-list
ab-elhaddad Apr 11, 2024
0c8304a
Desktop: refactor: renamed vars
ab-elhaddad Apr 11, 2024
7e82b61
Desktop: fix: moved checkbox to be within the font list
ab-elhaddad Apr 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/FontSearch.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ packages/app-desktop/gui/Button/Button.js
packages/app-desktop/gui/ClipperConfigScreen.js
packages/app-desktop/gui/ConfigScreen/ButtonBar.js
packages/app-desktop/gui/ConfigScreen/ConfigScreen.js
packages/app-desktop/gui/ConfigScreen/FontSearch.js
packages/app-desktop/gui/ConfigScreen/Sidebar.js
packages/app-desktop/gui/ConfigScreen/controls/MissingPasswordHelpLink.js
packages/app-desktop/gui/ConfigScreen/controls/ToggleAdvancedSettingsButton.js
Expand Down
52 changes: 39 additions & 13 deletions packages/app-desktop/gui/ConfigScreen/ConfigScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ButtonBar from './ButtonBar';
import Button, { ButtonLevel, ButtonSize } from '../Button/Button';
import { _ } from '@joplin/lib/locale';
import bridge from '../../services/bridge';
import Setting, { AppType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import Setting, { AppType, SettingItemSubType, SyncStartupOperation } from '@joplin/lib/models/Setting';
import control_PluginsStates from './controls/plugins/PluginsStates';
import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigScreen';
import { reg } from '@joplin/lib/registry';
Expand All @@ -20,12 +20,23 @@ import ToggleAdvancedSettingsButton from './controls/ToggleAdvancedSettingsButto
import shouldShowMissingPasswordWarning from '@joplin/lib/components/shared/config/shouldShowMissingPasswordWarning';
import MacOSMissingPasswordHelpLink from './controls/MissingPasswordHelpLink';
const { KeymapConfigScreen } = require('../KeymapConfig/KeymapConfigScreen');
import FontSearch from './FontSearch';

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
const settingKeyToControl: any = {
'plugins.states': control_PluginsStates,
};

interface Font {
family: string;
}

declare global {
interface Window {
queryLocalFonts(): Promise<Font[]>;
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
class ConfigScreenComponent extends React.Component<any, any> {

Expand All @@ -44,6 +55,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
screenName: '',
changedSettingKeys: [],
needRestart: false,
fonts: [],
};

this.rowStyle_ = {
Expand Down Expand Up @@ -78,12 +90,16 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.setState({ settings: this.props.settings });
}

public componentDidMount() {
public async componentDidMount() {
if (this.props.defaultSection) {
this.setState({ selectedSectionName: this.props.defaultSection }, () => {
void this.switchSection(this.props.defaultSection);
});
}

const fonts = (await window.queryLocalFonts()).map((font: Font) => font.family);
const uniqueFonts = [...new Set(fonts)];
this.setState({ fonts: uniqueFonts });
}

private async handleSettingButton(key: string) {
Expand Down Expand Up @@ -591,22 +607,32 @@ class ConfigScreenComponent extends React.Component<any, any> {
const onTextChange = (event: any) => {
updateSettingValue(key, event.target.value);
};

return (
<div key={key} style={rowStyle}>
<div style={labelStyle}>
<label>{md.label()}</label>
</div>
<input
type={inputType}
style={inputStyle}
value={this.state.settings[key]}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
onTextChange(event);
}}
spellCheck={false}
/>
{
md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ?
<FontSearch
type={inputType}
style={inputStyle}
value={this.state.settings[key]}
availableFonts={this.state.fonts}
onChange={fontFamily => updateSettingValue(key, fontFamily)}
subtype={md.subType}
/> :
<input
type={inputType}
style={inputStyle}
value={this.state.settings[key]}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
onChange={(event: any) => {
onTextChange(event);
}}
spellCheck={false}
/>
}
<div style={{ width: inputStyle.width, minWidth: inputStyle.minWidth }}>
{descriptionComp}
</div>
Expand Down
232 changes: 232 additions & 0 deletions packages/app-desktop/gui/ConfigScreen/FontSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import React = require('react');
import { useMemo, useState, useCallback, CSSProperties, useEffect, useRef } from 'react';
import { _ } from '@joplin/lib/locale';
import { SettingItemSubType } from '@joplin/lib/models/Setting';
import { focus } from '@joplin/lib/utils/focusHandler';

interface Props {
type: string;
style: CSSProperties;
value: string;
availableFonts: string[];
onChange: (font: string)=> void;
subtype: string;
}

const FontSearch = (props: Props) => {
const { type, style, value, availableFonts, onChange, subtype } = props;
const [filteredAvailableFonts, setFilteredAvailableFonts] = useState(availableFonts);
const [inputText, setInputText] = useState(value);
const [showList, setShowList] = useState(false);
const [isListHovered, setIsListHovered] = useState(false);
const [isFontSelected, setIsFontSelected] = useState(value !== '');
const [visibleFonts, setVisibleFonts] = useState<string[]>([]);
const [isMonoBoxChecked, setIsMonoBoxChecked] = useState(false);
const isLoadingFonts = filteredAvailableFonts.length === 0;
const fontInputRef = useRef<HTMLInputElement>(null);

useEffect(() => {
if (subtype === SettingItemSubType.MonospaceFontFamily) {
setIsMonoBoxChecked(true);
}
}, [subtype]);

useEffect(() => {
if (!isMonoBoxChecked) return setFilteredAvailableFonts(availableFonts);
const localMonospacedFonts = availableFonts.filter((font: string) =>
monospaceKeywords.some((word: string) => font.toLowerCase().includes(word)) ||
knownMonospacedFonts.includes(font.toLowerCase()),
);
setFilteredAvailableFonts(localMonospacedFonts);
}, [isMonoBoxChecked, availableFonts]);

const displayedFonts = useMemo(() => {
if (isFontSelected) return filteredAvailableFonts;
return filteredAvailableFonts.filter((font: string) =>
font.toLowerCase().startsWith(inputText.toLowerCase()),
);
}, [filteredAvailableFonts, inputText, isFontSelected]);

useEffect(() => {
setVisibleFonts(displayedFonts.slice(0, 20));
}, [displayedFonts]);

// Lazy loading
const handleListScroll: React.UIEventHandler<HTMLDivElement> = useCallback((event) => {
const scrollTop = (event.target as HTMLDivElement).scrollTop;
const scrollHeight = (event.target as HTMLDivElement).scrollHeight;
const clientHeight = (event.target as HTMLDivElement).clientHeight;

// Check if the user has scrolled to the bottom of the container
// A small buffer of 20 pixels is subtracted from the total scrollHeight to ensure new content starts loading slightly before the user reaches the absolute bottom, providing a smoother experience.
if (scrollTop + clientHeight >= scrollHeight - 20) {
// Load the next 20 fonts
const remainingFonts = displayedFonts.slice(visibleFonts.length, visibleFonts.length + 20);

setVisibleFonts([...visibleFonts, ...remainingFonts]);
}
}, [displayedFonts, visibleFonts]);

const handleTextChange: React.ChangeEventHandler<HTMLInputElement> = useCallback((event) => {
setIsFontSelected(false);
setInputText(event.target.value);
onChange(event.target.value);
}, [onChange]);

const handleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback(() => setShowList(true), []);

const handleBlur: React.FocusEventHandler<HTMLInputElement> = useCallback(() => {
if (!isListHovered) {
setShowList(false);
}
}, [isListHovered]);

const handleFontClick: React.MouseEventHandler<HTMLDivElement> = useCallback((event) => {
const font = (event.target as HTMLDivElement).innerText;
setInputText(font);
setShowList(false);
onChange(font);
setIsFontSelected(true);
}, [onChange]);

const handleListHover: React.MouseEventHandler<HTMLDivElement> = useCallback(() => setIsListHovered(true), []);

const handleListLeave: React.MouseEventHandler<HTMLDivElement> = useCallback(() => setIsListHovered(false), []);

const handleMonoBoxCheck: React.ChangeEventHandler<HTMLInputElement> = useCallback(() => {
setIsMonoBoxChecked(!isMonoBoxChecked);
focus('FontSearch::fontInputRef', fontInputRef.current);
}, [isMonoBoxChecked]);

return (
<>
<input
type={type}
style={style}
value={inputText}
onChange={handleTextChange}
onFocus={handleFocus}
onBlur={handleBlur}
spellCheck={false}
ref={fontInputRef}
/>
<div
className={'font-search-list'}
style={{ display: showList ? 'block' : 'none' }}
onMouseEnter={handleListHover}
onMouseLeave={handleListLeave}
onScroll={handleListScroll}
>
{
isLoadingFonts ? <div>{_('Loading...')}</div> :
<>
<div className='monospace-checkbox'>
<input
type='checkbox'
checked={isMonoBoxChecked}
onChange={handleMonoBoxCheck}
id={`show-monospace-fonts_${subtype}`}
/>
<label htmlFor={`show-monospace-fonts_${subtype}`}>{_('Show monospace fonts only.')}</label>
</div>
{
visibleFonts.map((font: string) =>
<div
key={font}
style={{ fontFamily: `"${font}"` }}
onClick={handleFontClick}
className='font-search-item'
>
{font}
</div>,
)
}
</>
}
</div>
</>
);
};

export default FontSearch;

// Known monospaced fonts from wikipedia
// https://en.wikipedia.org/wiki/List_of_monospaced_typefaces
// https://en.wikipedia.org/wiki/Category:Monospaced_typefaces
// Make sure to add the fonts in lower case
// cSpell:disable
const knownMonospacedFonts = [
'andalé mono',
'anonymous pro',
'bitstream vera sans mono',
'cascadia code',
'century schoolbook monospace',
'comic mono',
'computer modern mono/typewriter',
'consolas',
'courier',
'courier final draft',
'courier new',
'courier prime',
'courier screenplay',
'cousine',
'dejavu sans mono',
'droid sans mono',
'envy code r',
'everson mono',
'fantasque sans mono',
'fira code',
'fira mono',
'fixed',
'fixedsys',
'freemono',
'go mono',
'hack',
'hyperfont',
'ibm courier',
'ibm plex mono',
'inconsolata',
'input',
'iosevka',
'jetbrains mono',
'juliamono',
'letter gothic',
'liberation mono',
'lucida console',
'menlo',
'monaco',
'monofur',
'monospace (unicode)',
'nimbus mono l',
'nk57 monospace',
'noto mono',
'ocr-a',
'ocr-b',
'operator mono',
'overpass mono',
'oxygen mono',
'pragmatapro',
'profont',
'pt mono',
'recursive mono',
'roboto mono',
'sf mono',
'source code pro',
'spleen',
'terminal',
'terminus',
'tex gyre cursor',
'ubuntu mono',
'victor mono',
'wumpus mono',
];

const monospaceKeywords = [
'mono',
'code',
'courier',
'console',
'source code',
'terminal',
'fixed',
];
32 changes: 32 additions & 0 deletions packages/app-desktop/gui/ConfigScreen/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,35 @@
.config-screen-content > .section:last-child {
border-bottom: 0;
}

.font-search-list {
background-color: var(--joplin-background-color);
max-height: 200px;
width: 50%;
min-width: 20em;
overflow-y: auto;
border: 1px solid var(--joplin-border-color4);
border-radius: 5px;
display: none;
margin-top: 10px;
}

.font-search-list > div {
padding: 5px;
}

.font-search-item {
border-bottom: 1px solid var(--joplin-border-color4);
cursor: pointer;
}

.font-search-item:hover {
color: var(--joplin-background-color);
background-color: var(--joplin-color);
}

.monospace-checkbox {
background-color: var(--joplin-background-color3);
display: flex;
align-items: center;
}
Loading
Loading