Skip to content

Commit

Permalink
Merge pull request #6732 from kursat/job-5142
Browse files Browse the repository at this point in the history
  • Loading branch information
thienlnam authored Jan 3, 2022
2 parents c043e3d + 41d7744 commit 4151943
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 30 deletions.
117 changes: 90 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
"@react-navigation/native": "6.0.5",
"@react-navigation/stack": "6.0.10",
"babel-plugin-transform-remove-console": "^6.9.4",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
"dotenv": "^8.2.0",
"electron-context-menu": "^2.3.0",
"electron-log": "^4.3.5",
Expand All @@ -65,6 +67,7 @@
"expensify-common": "git+https://github.com/Expensify/expensify-common.git#fa190f6c844cf5646345f3e5e4862b62f1fa27bc",
"file-loader": "^6.0.0",
"html-entities": "^1.3.1",
"htmlparser2": "^7.2.0",
"lodash": "4.17.21",
"metro-config": "^0.64.0",
"moment": "^2.27.0",
Expand Down
5 changes: 5 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ const CONST = {
shortcutKey: 'Enter',
modifiers: [],
},
COPY: {
descriptionKey: 'copy',
shortcutKey: 'C',
modifiers: ['CTRL'],
},
},
KEYBOARD_SHORTCUT_KEY_DISPLAY_NAME: {
CONTROL: 'Ctrl',
Expand Down
3 changes: 2 additions & 1 deletion src/components/PressableWithSecondaryInteraction/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _ from 'underscore';
import React, {Component} from 'react';
import {Pressable} from 'react-native';
import SelectionScraper from '../../libs/SelectionScraper';
import * as pressableWithSecondaryInteractionPropTypes from './pressableWithSecondaryInteractionPropTypes';
import styles from '../../styles/styles';

Expand Down Expand Up @@ -30,7 +31,7 @@ class PressableWithSecondaryInteraction extends Component {
* https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
*/
executeSecondaryInteractionOnContextMenu(e) {
const selection = window.getSelection().toString();
const selection = SelectionScraper.getAsMarkdown();
e.stopPropagation();
if (this.props.preventDefaultContentMenu) {
e.preventDefault();
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ export default {
escape: 'Escape Dialogs',
search: 'Open search dialog',
newGroup: 'New group screen',
copy: 'Copy comment',
},
},
guides: {
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,7 @@ export default {
escape: 'Diálogos de escape',
search: 'Abrir diálogo de búsqueda',
newGroup: 'Nueva pantalla de grupo',
copy: 'Copiar comentario',
},
},
guides: {
Expand Down
142 changes: 142 additions & 0 deletions src/libs/SelectionScraper/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import render from 'dom-serializer';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import {parseDocument} from 'htmlparser2';
import {Element} from 'domhandler';
import _ from 'underscore';

const elementsWillBeSkipped = ['html', 'body'];
const tagAttribute = 'data-testid';

/**
* Reads html of selection. If browser doesn't support Selection API, returns empty string.
* @returns {String} HTML of selection as String
*/
const getHTMLOfSelection = () => {
if (window.getSelection) {
const selection = window.getSelection();

if (selection.rangeCount > 0) {
const div = document.createElement('div');

// HTML tag of markdown comments is in data-testid attribute (em, strong, blockquote..). Our goal here is to
// find that nodes and replace that tag with the one inside data-testid, so ExpensiMark can parse it.
// Simply, we want to replace this:
// <span class="..." style="..." data-testid="strong">bold</span>
// to this:
// <strong>bold</strong>
//
// We traverse all ranges, and get closest node with data-testid and replace its contents with contents of
// range.
for (let i = 0; i < selection.rangeCount; i++) {
const range = selection.getRangeAt(i);

const clonedSelection = range.cloneContents();

// If clonedSelection has no text content this data has no meaning to us.
if (clonedSelection.textContent) {
let node = null;

// If selection starts and ends within same text node we use its parentNode. This is because we can't
// use closest function on a [Text](https://developer.mozilla.org/en-US/docs/Web/API/Text) node.
// We are selecting closest node because nodes with data-testid can be one of the parents of the actual node.
// Assuming we selected only "block" part of following html:
// <div className="..." style="..." data-testid="pre">
// <div dir="auto" class="..." style="...">
// this is block code
// </div>
// </div>
// commonAncestorContainer: #text "this is block code"
// commonAncestorContainer.parentNode:
// <div dir="auto" class="..." style="...">
// this is block code
// </div>
// and finally commonAncestorContainer.parentNode.closest('data-testid') is targeted dom.
if (range.commonAncestorContainer instanceof HTMLElement) {
node = range.commonAncestorContainer.closest(`[${tagAttribute}]`);
} else {
node = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`);
}

// This means "range.commonAncestorContainer" is a text node. We simply get its parent node.
if (!node) {
node = range.commonAncestorContainer.parentNode;
}

node = node.cloneNode();
node.appendChild(clonedSelection);
div.appendChild(node);
}
}

return div.innerHTML;
}

return window.getSelection().toString();
}

// If browser doesn't support Selection API, returns empty string.
return '';
};

/**
* Clears all attributes from dom elements
* @param {Object} dom htmlparser2 dom representation
* @returns {Object} htmlparser2 dom representation
*/
const replaceNodes = (dom) => {
let domName = dom.name;
let domChildren;
const domAttribs = {};

// We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data
// has no meaning for us.
if (dom.attribs && dom.attribs[tagAttribute]) {
if (!elementsWillBeSkipped.includes(dom.attribs[tagAttribute])) {
domName = dom.attribs[tagAttribute];
}

// Adding a new line after each comment here, because adding after each range is not working for chrome.
if (dom.attribs[tagAttribute] === 'comment') {
dom.children.push(new Element('br', {}));
}
}

// We need to preserve href attribute in order to copy links.
if (dom.attribs && dom.attribs.href) {
domAttribs.href = dom.attribs.href;
}

if (dom.children) {
domChildren = _.map(dom.children, c => replaceNodes(c));
}

return {
...dom,
name: domName,
attribs: domAttribs,
children: domChildren,
};
};

/**
* Reads html of selection, replaces with proper tags used for markdown, parses to markdown.
* @returns {String} parsed html as String
*/
const getAsMarkdown = () => {
const selectionHtml = getHTMLOfSelection();

const domRepresentation = parseDocument(selectionHtml);
domRepresentation.children = _.map(domRepresentation.children, c => replaceNodes(c));

const newHtml = render(domRepresentation);

const parser = new ExpensiMark();

return parser.htmlToMarkdown(newHtml);
};

const SelectionScraper = {
getAsMarkdown,
};

export default SelectionScraper;
9 changes: 9 additions & 0 deletions src/libs/SelectionScraper/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* This is a no-op component for native devices because they wouldn't be able to support Selection API like
* a website.
*/
const SelectionParser = {
getAsMarkdown: () => '',
};

export default SelectionParser;
9 changes: 7 additions & 2 deletions src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import * as Expensicons from '../../../../components/Icon/Expensicons';
import * as Report from '../../../../libs/actions/Report';
import Clipboard from '../../../../libs/Clipboard';
Expand Down Expand Up @@ -49,7 +49,12 @@ export default [
onPress: (closePopover, {reportAction, selection}) => {
const message = _.last(lodashGet(reportAction, 'message', null));
const html = lodashGet(message, 'html', '');
const text = Str.htmlDecode(selection || lodashGet(message, 'text', ''));

const parser = new ExpensiMark();
const reportMarkdown = parser.htmlToMarkdown(html);

const text = selection || reportMarkdown;

const isAttachment = _.has(reportAction, 'isAttachment')
? reportAction.isAttachment
: ReportUtils.isReportMessageAttachment(text);
Expand Down
Loading

0 comments on commit 4151943

Please sign in to comment.