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

Inline text formatting #1323

Merged
merged 63 commits into from May 6, 2020
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
536ac78
WIP: First partially working version of inline text formatting
Apr 23, 2020
2e023e8
Merge branch 'master' into feature/inline-rich-text
Apr 23, 2020
854510f
Added fix for importing elements with multiple styles
Apr 23, 2020
6fdac4c
Added the option to toggle b/i/u for entire text field
Apr 23, 2020
e0506bf
Cleaned up the structure a bit
Apr 23, 2020
323ec6c
Added support for font weight and multi-select
Apr 24, 2020
6377f5a
Update tests now that some values are not in use
Apr 24, 2020
48c765d
Adding `npx` to postinstall to see if CI approves
Apr 24, 2020
b0ba301
Removed `postinstall-postinstall`
Apr 24, 2020
1656c0a
If selection is collapsed when applying style change, apply to inline…
Apr 24, 2020
ad34f87
Reorganised selection manip and added letter spacing
Apr 24, 2020
4a28da7
Disable force focus when setting letter spacing
Apr 24, 2020
6346fa2
Added inline text color formatting
Apr 24, 2020
ebf8f26
Fix multiple reducer for patterns, and fix tests
Apr 24, 2020
04af4f5
Fixed font and font weight dropdowns
Apr 24, 2020
e17c4f6
Remove deprecated types from text element
Apr 24, 2020
47df628
Some extra cleanup of deprecated properties
Apr 24, 2020
5dcaf4e
Added new migration with test (also added `jest-extended`)
Apr 27, 2020
0f49ebd
Merge branch 'master' into feature/inline-rich-text
Apr 27, 2020
e8b1f4e
Merge branch 'master' into feature/inline-rich-text
Apr 28, 2020
9739dfb
Refactored formatters to verticals rather than horizontals - much cle…
Apr 29, 2020
41e6954
Fixed reducer and added comments
Apr 29, 2020
97a91cf
Added jsdocs to html manipulation
Apr 29, 2020
c4f4c18
Removed classes from migration and added test for nested tags
Apr 29, 2020
057f33c
Fix tests for text style now that export does not have classes anymore
Apr 29, 2020
8097c3f
Moved focus management to edit component
Apr 29, 2020
8685d11
Create general `getValidHTML` util
Apr 29, 2020
3a80904
Added deps to `useImperativeHandle`
Apr 29, 2020
e2d2f23
Fixed `parseFloat` args
Apr 29, 2020
4f58366
Merge branch 'master' into feature/inline-rich-text
Apr 29, 2020
a0b3259
Added faux-selection style and fixed various bugs
Apr 29, 2020
59134bc
Fixed typo
Apr 29, 2020
ab85df5
For now, strip formatting when pasting
Apr 29, 2020
37c9433
Merge branch 'master' into feature/inline-rich-text
Apr 29, 2020
2f09bc6
Remove weird npm scripts
Apr 29, 2020
7e2ed36
Using a less nice matcher
Apr 29, 2020
102c5af
Added tests for formatters
Apr 30, 2020
4f88010
Merge branch 'master' into feature/inline-rich-text
Apr 30, 2020
aa2fff6
Merge branch 'master' into feature/inline-rich-text
Apr 30, 2020
44d7a77
Set default font color to black - all other colors are fixed inline
Apr 30, 2020
b5a0c76
Added unit tests for style manipulation
May 1, 2020
080251b
Merge branch 'master' into feature/inline-rich-text
May 1, 2020
85edb58
Fixed color input (allow multiple value string)
May 1, 2020
e8f6266
Fixed color picker to default to solid black (rather than transparent…
May 1, 2020
f1dcb73
Fixed null value for input
May 1, 2020
03aa64b
Fix color preset to work for new ITF formatting
May 1, 2020
1b4f9de
Added full support for text color and text style presets
May 1, 2020
f0dd127
Inlined font weights for text panel presets
May 1, 2020
d411b08
Fix focus out to check click target in capture phase
May 1, 2020
f1c8468
Merge branch 'master' into feature/inline-rich-text
May 4, 2020
16dbf27
Re-add default props to test
May 4, 2020
e295b37
Added jsdoc for draftUtils
May 4, 2020
870f2bd
Fix children proptype
May 4, 2020
e03baf3
Add better error handlign to faux selection removal
May 4, 2020
0081a79
Added jsdocs for complex style manipulation functions
May 4, 2020
c5adfec
Added function to strip relevant inline styling
May 4, 2020
cd77cb9
Merge branch 'master' into feature/inline-rich-text
May 5, 2020
7a28375
Merge branch 'master' into feature/inline-rich-text
May 5, 2020
ba711d7
Added new function to compare patterns and used where applicable
May 5, 2020
b63842a
Added ignore black to set color style as it is default
May 6, 2020
b6468b0
Changed highlight to use existing style manipulation
May 6, 2020
595d4dd
Merge branch 'master' into feature/inline-rich-text
May 6, 2020
13b6fda
Removed conditional hook
May 6, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 9 additions & 1 deletion assets/src/edit-story/components/panels/test/textStyle.js
Expand Up @@ -391,7 +391,15 @@ describe('Panels/TextStyle', () => {
it('should select font weight', () => {
const { pushUpdate } = renderTextStyle([textElement]);
act(() => controls['font.weight'].onChange('300'));
expect(pushUpdate).toHaveBeenCalledWith({ fontWeight: 300 }, true);
const updatingFunction = pushUpdate.mock.calls[0][0];
const resultOfUpdating = updatingFunction({ content: 'Hello world' });
expect(resultOfUpdating).toStrictEqual(
{
content:
'<span class="weight" style="font-weight: 300">Hello world</span>',
},
true
);
});

it('should select font size', () => {
Expand Down
11 changes: 7 additions & 4 deletions assets/src/edit-story/components/panels/textStyle/font.js
Expand Up @@ -33,6 +33,7 @@ import { Numeric, Row, DropDown } from '../../form';
import { PAGE_HEIGHT } from '../../../constants';
import { useFont } from '../../../app/font';
import { getCommonValue } from '../utils';
import useRichTextFormatting from './useRichTextFormatting';

const Space = styled.div`
flex: 0 0 10px;
Expand All @@ -46,7 +47,11 @@ const BoxedNumeric = styled(Numeric)`
function FontControls({ selectedElements, pushUpdate }) {
const fontFamily = getCommonValue(selectedElements, 'fontFamily');
const fontSize = getCommonValue(selectedElements, 'fontSize');
const fontWeight = getCommonValue(selectedElements, 'fontWeight');

const {
textInfo: { fontWeight },
handlers: { handleSelectFontWeight },
} = useRichTextFormatting(selectedElements, pushUpdate);

const {
state: { fonts },
Expand Down Expand Up @@ -103,9 +108,7 @@ function FontControls({ selectedElements, pushUpdate }) {
ariaLabel={__('Font weight', 'web-stories')}
options={fontWeights}
value={fontWeight}
onChange={(value) =>
pushUpdate({ fontWeight: parseInt(value) }, true)
}
onChange={handleSelectFontWeight}
/>
<Space />
</>
Expand Down
25 changes: 12 additions & 13 deletions assets/src/edit-story/components/panels/textStyle/textStyle.js
Expand Up @@ -40,6 +40,7 @@ import { ReactComponent as BoldIcon } from '../../../icons/bold_icon.svg';
import { ReactComponent as ItalicIcon } from '../../../icons/italic_icon.svg';
import { ReactComponent as UnderlineIcon } from '../../../icons/underline_icon.svg';
import { getCommonValue } from '../utils';
import useRichTextFormatting from './useRichTextFormatting';

const BoxedNumeric = styled(Numeric)`
padding: 6px 6px;
Expand All @@ -64,9 +65,11 @@ function StylePanel({ selectedElements, pushUpdate }) {
const textAlign = getCommonValue(selectedElements, 'textAlign');
const letterSpacing = getCommonValue(selectedElements, 'letterSpacing');
const lineHeight = getCommonValue(selectedElements, 'lineHeight');
const fontStyle = getCommonValue(selectedElements, 'fontStyle');
const textDecoration = getCommonValue(selectedElements, 'textDecoration');
const bold = getCommonValue(selectedElements, 'bold');

const {
textInfo: { isBold, isItalic, isUnderline },
handlers: { handleClickBold, handleClickItalic, handleClickUnderline },
} = useRichTextFormatting(selectedElements, pushUpdate);

return (
<>
Expand Down Expand Up @@ -128,28 +131,24 @@ function StylePanel({ selectedElements, pushUpdate }) {
/>
<ToggleButton
icon={<BoldIcon />}
value={bold === true}
value={isBold}
iconWidth={9}
iconHeight={10}
onChange={(value) => pushUpdate({ bold: value }, true)}
onChange={handleClickBold}
/>
<ToggleButton
icon={<ItalicIcon />}
value={fontStyle === 'italic'}
value={isItalic}
iconWidth={10}
iconHeight={10}
onChange={(value) =>
pushUpdate({ fontStyle: value ? 'italic' : 'normal' }, true)
}
onChange={handleClickItalic}
/>
<ToggleButton
icon={<UnderlineIcon />}
value={textDecoration === 'underline'}
value={isUnderline}
iconWidth={8}
iconHeight={21}
onChange={(value) =>
pushUpdate({ textDecoration: value ? 'underline' : 'none' }, true)
}
onChange={handleClickUnderline}
/>
</Row>
</>
Expand Down
@@ -0,0 +1,113 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import { useMemo, useCallback } from 'react';

/**
* Internal dependencies
*/
import useRichText from '../../richText/useRichText';
import {
getHTMLInfo,
toggleBoldInHTML,
setFontWeightInHTML,
toggleItalicInHTML,
toggleUnderlineInHTML,
} from '../../richText/htmlManipulation';
import { MULTIPLE_VALUE } from '../../form';

function reduceWithMultiple(reduced, info) {
barklund marked this conversation as resolved.
Show resolved Hide resolved
return Object.fromEntries(
Object.keys(info).map((key) => {
const wasMultiple = reduced[key] === MULTIPLE_VALUE;
const hadValue = typeof reduced[key] !== 'undefined';
const areDifferent = hadValue && reduced[key] !== info[key];
if (wasMultiple || areDifferent) {
return [key, MULTIPLE_VALUE];
}
return [key, info[key]];
})
);
}

function useRichTextFormatting(selectedElements, pushUpdate) {
const {
state: { hasCurrentEditor, selectionInfo },
actions: {
toggleBoldInSelection,
setFontWeightInSelection,
toggleItalicInSelection,
toggleUnderlineInSelection,
},
} = useRichText();

const textInfo = useMemo(() => {
if (hasCurrentEditor) {
return selectionInfo;
}

// loop over all elements, find info for content and reduce to common value
// (setting MULTIPLE_VALUE appropriately)
return selectedElements
.map(({ content }) => content)
.map(getHTMLInfo)
.reduce(reduceWithMultiple, {});
}, [hasCurrentEditor, selectionInfo, selectedElements]);

const push = useCallback(
(updater, ...args) =>
pushUpdate(
({ content }) => ({ content: updater(content, ...args) }),
true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all of the field updates are auto-submit, right? E.g. letter-spacing is not auto-submit. It's just a normal numeric input.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now updates the text field as soon as a value is entered. But the new content is (as always) not submitted until the edit mode is exited.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how this will go for letter-spacing? The sequence of input:

  1. 150 (initial value, indicating 150%)
  2. 15 (the right-most character is deleted)
  3. 1 (the right-most character is deleted)
  4. '' (the right-most character is deleted, this is not an empty string)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input handling in the input component here is not optimal - it'll push its changes on every change, and then re-read the current value from the real input, resulting in empty value not being allowed, so it's un-intuitive to type some values (e.g. you cannot clear the text field). That's an optimization of the input component though, not the text formatter IMHO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Let's proceed and we'll need to see how we can follow up on this. For now this is one of few items that allows intermediate values. It might be best to do this update via the form's usePresubmitHandler, but can be done as a follow up.

),
[pushUpdate]
);

const handlers = useMemo(() => {
if (hasCurrentEditor) {
return {
handleClickBold: toggleBoldInSelection,
handleSelectFontWeight: setFontWeightInSelection,
handleClickItalic: toggleItalicInSelection,
handleClickUnderline: toggleUnderlineInSelection,
};
}

return {
handleClickBold: (flag) => push(toggleBoldInHTML, flag),
handleSelectFontWeight: (weight) => push(setFontWeightInHTML, weight),
handleClickItalic: (flag) => push(toggleItalicInHTML, flag),
handleClickUnderline: (flag) => push(toggleUnderlineInHTML, flag),
};
}, [
hasCurrentEditor,
toggleBoldInSelection,
setFontWeightInSelection,
toggleItalicInSelection,
toggleUnderlineInSelection,
push,
]);

return {
textInfo,
handlers,
};
}

export default useRichTextFormatting;
24 changes: 24 additions & 0 deletions assets/src/edit-story/components/richText/context.js
@@ -0,0 +1,24 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import { createContext } from 'react';

const RichTextContext = createContext({ state: {}, actions: {} });

export default RichTextContext;
43 changes: 43 additions & 0 deletions assets/src/edit-story/components/richText/customConstants.js
@@ -0,0 +1,43 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export const NONE = 'NONE';
export const ITALIC = 'CUSTOM-ITALIC';
export const UNDERLINE = 'CUSTOM-UNDERLINE';
export const WEIGHT = 'CUSTOM-WEIGHT';
export const COLOR = 'CUSTOM-COLOR';
export const LETTERSPACING = 'CUSTOM-LETTERSPACING';

export const NORMAL_WEIGHT = 400;
export const SMALLEST_BOLD = 600;
export const DEFAULT_BOLD = 700;

export const weightToStyle = (weight) => `${WEIGHT}-${weight}`;
export const colorToStyle = (color) => `${COLOR}-${color}`;
export const lsToStyle = (ls) => `${LETTERSPACING}-${ls}`;

export const isStyle = (style, prefix) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity, would it have worked if we simply used here CSS notation. E.g. BOLD-700 becomes a custom style font-weight:700 (or fontWeight:700) instead? And so on. If possible, this could possibly simplify some of the downstream code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I'll think about it as I'll be refactoring this part of the codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not changed this part as of yet. It's still an open option though.

typeof style === 'string' && style.startsWith(prefix);
export const getVariable = (style, prefix) => style.slice(prefix.length + 1);

export const styleToWeight = (style) =>
isStyle(style, WEIGHT) ? parseInt(getVariable(style, WEIGHT)) : null;
export const styleToColor = (style) =>
isStyle(style, COLOR) ? getVariable(style, COLOR) : null;
export const styleToLs = (style) =>
isStyle(style, LETTERSPACING)
? parseInt(getVariable(style, LETTERSPACING))
: null;
87 changes: 87 additions & 0 deletions assets/src/edit-story/components/richText/customExport.js
@@ -0,0 +1,87 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* External dependencies
*/
import { stateToHTML } from 'draft-js-export-html';

/**
* Internal dependencies
*/
import { styleToWeight, ITALIC, UNDERLINE } from './customConstants';

function inlineStyleFn(styles) {
const inline = styles.toArray().reduce(
({ classes, css }, style) => {
// Italic
if (style === ITALIC) {
return {
classes: [...classes, 'italic'],
barklund marked this conversation as resolved.
Show resolved Hide resolved
css: { ...css, fontStyle: 'italic' },
};
}

// Underline
if (style === UNDERLINE) {
return {
classes: [...classes, 'underline'],
css: { ...css, textDecoration: 'underline' },
};
}

// Weight
const weight = styleToWeight(style);
if (weight) {
return {
classes: [...classes, 'weight'],
css: { ...css, fontWeight: weight },
};
}

// TODO: Color
// TODO: Letter spacing

return { classes, css };
},
{ classes: [], css: {} }
);

if (inline.classes.length === 0) {
return null;
}

return {
element: 'span',
attributes: { class: inline.classes.join(' ') },
style: inline.css,
};
}

function exportHTML(editorState) {
if (!editorState) {
return null;
}

const html = stateToHTML(editorState.getCurrentContent(), {
inlineStyleFn,
defaultBlockTag: null,
});

return html.replace(/<br ?\/?>/g, '').replace(/&nbsp;$/, '');
}

export default exportHTML;