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

Use editor3 internal clipboard to copy entities from open editors #2509

Merged
merged 11 commits into from
Sep 6, 2018
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
],
"main": "scripts/index.js",
"dependencies": {
"@types/draft-js": "^0.10.24",
Copy link
Member

Choose a reason for hiding this comment

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

It might not cause any issues, but I'd lock the version.

"@types/lodash": "4.14.116",
"@types/react": "16.4.9",
"angular": "1.6.9",
Expand All @@ -43,8 +44,6 @@
"angular-resource": "1.6.9",
"angular-route": "1.6.9",
"angular-vs-repeat": "1.1.7",
"ts-loader": "3.5.0",
"typescript": "3.0.1",
"bootstrap": "3.3.7",
"classnames": "2.2.5",
"css-loader": "0.28.10",
Expand Down Expand Up @@ -101,6 +100,8 @@
"shortid": "2.2.8",
"style-loader": "0.20.2",
"superdesk-ui-framework": "^1.1.0",
"ts-loader": "3.5.0",
"typescript": "3.0.1",
"webpack": "3.11.0",
"webpack-dev-server": "2.11.1"
},
Expand All @@ -118,6 +119,7 @@
"karma-ng-html2js-preprocessor": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.13",
"patch-package": "^5.1.1",
"protractor": "^5.1.2",
"react-addons-test-utils": "^15.6.0",
"react-test-renderer": "^16.2.0",
Expand All @@ -133,6 +135,7 @@
"lint": "tsc --noEmit --version && eslint --ext .js --ext .jsx --ext .tsx scripts spec tasks *.js",
"start-test-server": "cd test-server && python3 -m venv env && . env/bin/activate && pip install -Ur requirements.txt && honcho start",
"protractor": "protractor protractor.conf.js",
"webdriver-manager": "webdriver-manager"
"webdriver-manager": "webdriver-manager",
"postinstall": "patch-package"
}
}
19 changes: 19 additions & 0 deletions patches/@types/draft-js+0.10.24.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
patch-package
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ioanpocol I added a new dependency: patch-package. With it we can modify any package under node_modules and make a patch like this one to fix a bug without waiting for a PR to be merged. This will patch the module after an npm install automatically

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

--- a/node_modules/@types/draft-js/index.d.ts
+++ b/node_modules/@types/draft-js/index.d.ts
@@ -950,6 +950,7 @@ import Editor = Draft.Component.Base.DraftEditor;
import EditorProps = Draft.Component.Base.DraftEditorProps;
import EditorBlock = Draft.Component.Components.DraftEditorBlock;
import EditorState = Draft.Model.ImmutableData.EditorState;
+import EditorChangeType = Draft.Model.ImmutableData.EditorChangeType;

import CompositeDecorator = Draft.Model.Decorators.CompositeDraftDecorator;
import Entity = Draft.Model.Entity.DraftEntity;
@@ -999,6 +1000,7 @@ export {
EditorProps,
EditorBlock,
EditorState,
+ EditorChangeType,

CompositeDecorator,
Entity,
64 changes: 37 additions & 27 deletions scripts/core/editor3/components/Editor3Component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import {
Editor,
Expand Down Expand Up @@ -39,6 +38,8 @@ const VALID_MEDIA_TYPES = [
'Files',
];

export const EDITOR_GLOBAL_REFS = 'editor3-refs';

/**
* Get valid media type from event dataTransfer types
*
Expand All @@ -65,6 +66,32 @@ export function canDropMedia(e, editorConfig) {
return supportsMedia && isValidMedia;
}

export interface Editor3ComponentProps {
readOnly?: boolean,
locked?: boolean,
showToolbar?: boolean,
editorState?: any,
onChange?: Function,
unlock?: Function,
onTab?: (e: any) => void,
dragDrop?: Function,
scrollContainer: string,
singleLine?: boolean,
editorFormat?: Array<string>,
tabindex?: number,
dispatch?: Function,
suggestingMode?: boolean,
onCreateAddSuggestion?: Function,
onCreateDeleteSuggestion?: Function,
onPasteFromSuggestingMode?: Function,
onCreateSplitParagraphSuggestion?: Function,
onCreateChangeStyleSuggestion?: Function,
svc?: any,
invisibles?: boolean,
highlights?: any,
highlightsManager?: any,
}

/**
* @ngdoc React
* @module superdesk.core.editor3
Expand All @@ -77,7 +104,7 @@ export function canDropMedia(e, editorConfig) {
* @description Editor3 is a draft.js based editor that support customizable
* formatting, spellchecker and media files.
*/
export class Editor3Component extends React.Component<any, any> {
export class Editor3Component extends React.Component<Editor3ComponentProps, any> {
static propTypes: any;
static defaultProps: any;

Expand Down Expand Up @@ -323,6 +350,12 @@ export class Editor3Component extends React.Component<any, any> {

componentDidMount() {
$(this.div).on('dragover', this.onDragOver);

if (!window[EDITOR_GLOBAL_REFS]) {
window[EDITOR_GLOBAL_REFS] = {};
}

window[EDITOR_GLOBAL_REFS][this.editorKey] = this.editor;
}

handleRefs(editor) {
Expand All @@ -334,6 +367,8 @@ export class Editor3Component extends React.Component<any, any> {

componentWillUnmount() {
$(this.div).off();

delete window[EDITOR_GLOBAL_REFS][this.editorKey];
}

componentDidUpdate() {
Expand Down Expand Up @@ -422,31 +457,6 @@ export class Editor3Component extends React.Component<any, any> {
}
}

Editor3Component.propTypes = {
readOnly: PropTypes.bool,
locked: PropTypes.bool,
showToolbar: PropTypes.bool,
editorState: PropTypes.object,
onChange: PropTypes.func,
unlock: PropTypes.func,
onTab: PropTypes.func,
dragDrop: PropTypes.func,
scrollContainer: PropTypes.string.isRequired,
singleLine: PropTypes.bool,
editorFormat: PropTypes.array,
tabindex: PropTypes.number,
dispatch: PropTypes.func,
suggestingMode: PropTypes.bool,
onCreateAddSuggestion: PropTypes.func,
onCreateDeleteSuggestion: PropTypes.func,
onPasteFromSuggestingMode: PropTypes.func,
onCreateSplitParagraphSuggestion: PropTypes.func,
onCreateChangeStyleSuggestion: PropTypes.func,
svc: PropTypes.object,
invisibles: PropTypes.bool,
highlights: PropTypes.object,
highlightsManager: PropTypes.object,
};

Editor3Component.defaultProps = {
readOnly: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {EditorState, ContentState, Modifier, genKey, CharacterMetadata, ContentBlock} from 'draft-js';
import {List, OrderedSet} from 'immutable';
import {EditorState, ContentState, Modifier, genKey, CharacterMetadata, ContentBlock, DraftHandleValue, EditorChangeType} from 'draft-js';
import {EDITOR_GLOBAL_REFS, Editor3ComponentProps} from 'core/editor3/components/Editor3Component';
import {List, OrderedMap} from 'immutable';
import {getContentStateFromHtml} from '../html/from-html';
import * as Suggestions from '../helpers/suggestions';
import {sanitizeContent, inlineStyles} from '../helpers/inlineStyles';
import {getAllCustomDataFromEditor, setAllCustomDataForEditor} from '../helpers/editor3CustomData';
import {getCurrentAuthor} from '../helpers/author';
import {htmlComesFromDraftjsEditor} from '../helpers/htmlComesFromDraftjsEditor';

function removeMediaFromHtml(htmlString) {
function removeMediaFromHtml(htmlString) : string {
const element = document.createElement('div');

element.innerHTML = htmlString;
Expand All @@ -19,8 +20,26 @@ function removeMediaFromHtml(htmlString) {
return element.innerHTML;
}

const HANDLED = 'handled';
const NOT_HANDLED = 'not-handled';
function pasteContentFromOpenEditor(
html: string, editorState: EditorState, onChange: Function, editorFormat: Array<string>) : DraftHandleValue {
Copy link
Member

Choose a reason for hiding this comment

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

It's alright for now, but try avoiding the use of Function type, since it's very generic. You can define

interface IEditor3props {
    onChange(editorState: EditorState): void;
}

and then access onChange like a object property, so you have the whole function signature, but don't need to typed it entirely.

function functionWhichTakesOnChangeAsAnArgument(onChange: IEditor3props['onChange']) {
    // ...
}

for (const editorKey in window[EDITOR_GLOBAL_REFS]) {
if (html.includes(editorKey)) {
const editor = window[EDITOR_GLOBAL_REFS][editorKey];
const internalClipboard = editor.getClipboard();

if (internalClipboard) {
const blocksArray = [];

internalClipboard.forEach((b) => blocksArray.push(b));
Copy link
Member

Choose a reason for hiding this comment

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

Why aren't you passing internalClipboard to createFromBlockArray and instead are making a shallow copy?

const contentState = ContentState.createFromBlockArray(blocksArray);

return insertContentInState(editorState, contentState, onChange, editorFormat);
}
}
}

return 'not-handled';
Copy link
Member

Choose a reason for hiding this comment

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

What's wrong with DraftHandleValue?

}

/**
* @ngdoc method
Expand All @@ -31,46 +50,51 @@ const NOT_HANDLED = 'not-handled';
* @description Handles pasting into the editor, in cases where the content contains
* atomic blocks that need special handling in editor3.
*/
export function handlePastedText(text, _html) {
export function handlePastedText(text: string, _html: string) : DraftHandleValue {
const author = getCurrentAuthor();
let html = _html;

if (typeof html === 'string') {
html = removeMediaFromHtml(html);
}

const {editorState, suggestingMode, onPasteFromSuggestingMode} = this.props;
const {editorState, suggestingMode, onPasteFromSuggestingMode, onChange, editorFormat} = this.props;

if (!html && !text) {
return HANDLED;
return 'handled';
}

if (suggestingMode) {
if (!Suggestions.allowEditSuggestionOnLeft(editorState, author)
&& !Suggestions.allowEditSuggestionOnRight(editorState, author)) {
return HANDLED;
return 'handled';
}

const content = html ? getContentStateFromHtml(html) : ContentState.createFromText(text);

onPasteFromSuggestingMode(content);
return HANDLED;
return 'handled';
}

if (pasteContentFromOpenEditor(html, editorState, onChange, editorFormat) === 'handled') {
return 'handled';
}


if (htmlComesFromDraftjsEditor(html)) {
return NOT_HANDLED;
return 'not-handled';
}

return processPastedHtml(this.props, html || text);
}

// Checks if there are atomic blocks in the paste content. If there are, we need to set
// the 'atomic' block type using the Modifier tool and add these entities to the
// contentState.
function processPastedHtml(props, html) {
const {onChange, editorState, editorFormat} = props;
let pastedContent = getContentStateFromHtml(html);
const blockMap = pastedContent.getBlockMap();
function insertContentInState(
editorState: EditorState,
pastedContent: ContentState,
onChange: Function,
editorFormat: Array<string>) : DraftHandleValue {
let _pastedContent = pastedContent;
const blockMap = _pastedContent.getBlockMap();
const hasAtomicBlocks = blockMap.some((block) => block.getType() === 'atomic');
const acceptedInlineStyles =
Object.keys(inlineStyles)
Expand All @@ -86,7 +110,7 @@ function processPastedHtml(props, html) {
selection = contentState.getSelectionAfter();
}

pastedContent = sanitizeContent(EditorState.createWithContent(pastedContent), acceptedInlineStyles)
_pastedContent = sanitizeContent(EditorState.createWithContent(_pastedContent), acceptedInlineStyles)
.getCurrentContent();

blockMap.forEach((block) => {
Expand All @@ -95,7 +119,7 @@ function processPastedHtml(props, html) {
}

const entityKey = block.getEntityAt(0);
const entity = pastedContent.getEntity(entityKey);
const entity = _pastedContent.getEntity(entityKey);

contentState = contentState.addEntity(entity);

Expand All @@ -109,9 +133,11 @@ function processPastedHtml(props, html) {
contentState = Modifier.setBlockType(contentState, selection, 'atomic');
}

const newBlockMap = OrderedMap<string, ContentBlock>(blocks.map((b) => ([b.getKey(), b])));

let nextEditorState = EditorState.push(
editorState,
Modifier.replaceWithFragment(contentState, selection, OrderedSet(blocks)),
Modifier.replaceWithFragment(contentState, selection, newBlockMap),
'insert-fragment'
);

Expand All @@ -133,7 +159,21 @@ function processPastedHtml(props, html) {

onChange(nextEditorState);

return HANDLED;
return 'handled';
}

// Checks if there are atomic blocks in the paste content. If there are, we need to set
// the 'atomic' block type using the Modifier tool and add these entities to the
// contentState.
function processPastedHtml(props: Editor3ComponentProps, html: string) : DraftHandleValue {
let pastedContent = getContentStateFromHtml(html);

return insertContentInState(
props.editorState,
pastedContent,
props.onChange,
props.editorFormat
);
}

// Returns an empty block.
Expand Down
8 changes: 5 additions & 3 deletions scripts/core/editor3/components/tests/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function stateWithLink() {

return RichUtils.toggleLink(
EditorState.createWithContent(contentState),
linkSelection,
linkSelection as SelectionState,
entityKey
);
}
Expand Down Expand Up @@ -174,8 +174,10 @@ export function cursorAtPosition(editorState, pos, n = 0) {
.getFirstBlock()
.getKey();

return EditorState.forceSelection(editorState, SelectionState.createEmpty(blockKey).merge({
const selection = SelectionState.createEmpty(blockKey).merge({
anchorOffset: pos,
focusOffset: pos + n,
}));
}) as SelectionState;

return EditorState.forceSelection(editorState, selection);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {getDraftSelectionForEntireContent} from './getDraftSelectionForEntireCon
import {resizeDraftSelection} from './resizeDraftSelection';
import {clearInlineStyles} from './clearInlineStyles';
import {changeSuggestionsTypes, blockStylesDescription, paragraphSuggestionTypes} from '../highlightsConfig';
import _ from 'lodash';
Copy link
Member

Choose a reason for hiding this comment

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

import {onlyWhatYouNeed} from 'lodash', so we can have a smaller bundle size.


export const paragraphSeparator = '¶';

Expand Down Expand Up @@ -266,7 +267,8 @@ export function canAddHighlight(editorState, highlightType) {
* @param {Boolean} firstFound - if true return first style found otherwise return all styles found
* @description the highlight style from the new possition specified by offset.
*/
export function getHighlightStyleAtOffset(editorState, types, selection, offset, fromEnd = false, firstFound = true) {
export function getHighlightStyleAtOffset(
editorState, types, selection, offset, fromEnd = false, firstFound = true) : string | Array<string> {
const {block, newOffset} = getBlockAndOffset(editorState, selection, offset, fromEnd);

if (block == null) {
Expand Down Expand Up @@ -354,7 +356,7 @@ export function getHighlightAuthor(editorState, style) {
* @description the highlight associated data from the new possition specified by offset.
*/
export function getHighlightDataAtOffset(editorState, types, selection, offset, fromEnd = false) {
const style = getHighlightStyleAtOffset(editorState, types, selection, offset, fromEnd);
const style = getHighlightStyleAtOffset(editorState, types, selection, offset, fromEnd) as string;

if (style == null) {
return null;
Expand Down
2 changes: 1 addition & 1 deletion scripts/core/editor3/html/from-html/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ class HTMLParser {
* @param {string} html
* @param {Object} associations (optional)
*/
export function getContentStateFromHtml(html, associations) {
export function getContentStateFromHtml(html, associations = null) {
return new HTMLParser(html, associations).contentState();
}

Expand Down
Loading