mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-27 10:28:31 +08:00
WIP
This commit is contained in:
parent
7379952a8f
commit
ee49db6a09
10 changed files with 85 additions and 131 deletions
|
@ -35,12 +35,9 @@ class ProductsExtension extends ComposerExtension
|
|||
return ["with the word '#{word}'?"]
|
||||
return []
|
||||
|
||||
@applyTransformsToDraft: ({draft}) ->
|
||||
@applyTransformsToBody: ({fragment, draft}) ->
|
||||
if @warningsForSending({draft})
|
||||
updated = draft.clone()
|
||||
updated.body += "<br>This email \
|
||||
contains competitor's product names \
|
||||
or trademarks used in context."
|
||||
return updated
|
||||
return draft
|
||||
el = document.createElement('p');
|
||||
el.innerText = "This email contains competitor's product names or trademarks used in context."
|
||||
fragment.childNodes[0].appendChild(el)
|
||||
```
|
||||
|
|
|
@ -2,20 +2,18 @@ marked = require 'marked'
|
|||
Utils = require './utils'
|
||||
{ComposerExtension} = require 'nylas-exports'
|
||||
|
||||
|
||||
rawBodies = {}
|
||||
|
||||
class MarkdownComposerExtension extends ComposerExtension
|
||||
|
||||
@applyTransformsToDraft: ({draft}) ->
|
||||
nextDraft = draft.clone()
|
||||
rawBodies[draft.clientId] = nextDraft.body
|
||||
nextDraft.body = marked(Utils.getTextFromHtml(draft.body))
|
||||
return nextDraft
|
||||
@applyTransformsToDraft: ({fragment, draft}) ->
|
||||
root = fragment.childNodes[0]
|
||||
rawBodies[draft.clientId] = root.innerHTML
|
||||
root.innerHTML = marked(root.innerText)
|
||||
|
||||
@unapplyTransformsToDraft: ({draft}) ->
|
||||
nextDraft = draft.clone()
|
||||
nextDraft.body = rawBodies[nextDraft.clientId] ? nextDraft.body
|
||||
return nextDraft
|
||||
@unapplyTransformsToDraft: ({fragment, draft}) ->
|
||||
if rawBodies[draft.clientId]
|
||||
root = fragment.childNodes[0]
|
||||
root.innerHTML = rawBodies[draft.clientId]
|
||||
|
||||
module.exports = MarkdownComposerExtension
|
||||
|
|
|
@ -163,11 +163,19 @@ export default class SpellcheckComposerExtension extends ComposerExtension {
|
|||
});
|
||||
}
|
||||
|
||||
static applyTransformsToDraft = ({draft}) => {
|
||||
const nextDraft = draft.clone();
|
||||
nextDraft.body = nextDraft.body.replace(/<\/?spelling[^>]*>/g, '');
|
||||
return nextDraft;
|
||||
static applyTransformsToBody = ({fragment}) => {
|
||||
const spellingEls = fragment.querySelectorAll('spelling');
|
||||
for (const spellingEl of Array.from(spellingEls)) {
|
||||
// move contents out of the spelling node, remove the node
|
||||
const parent = spellingEl.parentNode;
|
||||
while (spellingEl.firstChild) {
|
||||
parent.insertBefore(spellingEl.firstChild, spellingEl);
|
||||
}
|
||||
parent.removeChild(spellingEl);
|
||||
}
|
||||
}
|
||||
|
||||
static unapplyTransformsToDraft = () => 'unnecessary'
|
||||
static unapplyTransformsToBody = () => {
|
||||
// no need to put spelling nodes back!
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import {NylasSpellchecker, Message} from 'nylas-exports';
|
|||
|
||||
const initialPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-before.html');
|
||||
const initialHTML = fs.readFileSync(initialPath).toString();
|
||||
const expectedPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-after.html');
|
||||
const expectedHTML = fs.readFileSync(expectedPath).toString();
|
||||
const afterPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-after.html');
|
||||
const afterHTML = fs.readFileSync(afterPath).toString();
|
||||
|
||||
describe('SpellcheckComposerExtension', function spellcheckComposerExtension() {
|
||||
beforeEach(() => {
|
||||
|
@ -30,23 +30,17 @@ describe('SpellcheckComposerExtension', function spellcheckComposerExtension() {
|
|||
};
|
||||
|
||||
SpellcheckComposerExtension.update(editor);
|
||||
expect(node.innerHTML).toEqual(expectedHTML);
|
||||
expect(node.innerHTML).toEqual(afterHTML);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyTransformsToDraft", () => {
|
||||
describe("applyTransformsToBody", () => {
|
||||
it("removes the spelling annotations it inserted", () => {
|
||||
const draft = new Message({ body: expectedHTML });
|
||||
const out = SpellcheckComposerExtension.applyTransformsToDraft({draft});
|
||||
expect(out.body).toEqual(initialHTML);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unapplyTransformsToDraft", () => {
|
||||
it("returns the magic no-op option", () => {
|
||||
const draft = new Message({ body: expectedHTML });
|
||||
const out = SpellcheckComposerExtension.unapplyTransformsToDraft({draft});
|
||||
expect(out).toEqual('unnecessary');
|
||||
const draft = new Message({ body: afterHTML });
|
||||
const range = document.createRange();
|
||||
const fragment = range.createContextualFragment(`<root>${afterHTML}</root>`);
|
||||
SpellcheckComposerExtension.applyTransformsToBody({fragment, draft});
|
||||
expect(fragment.childNodes[0].innerHTML).toEqual(initialHTML);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,20 +10,18 @@ export default class TemplatesComposerExtension extends ComposerExtension {
|
|||
return warnings;
|
||||
}
|
||||
|
||||
static applyTransformsToDraft = ({draft}) => {
|
||||
const nextDraft = draft.clone();
|
||||
nextDraft.body = nextDraft.body.replace(/<\/?code[^>]*>/g, (match) =>
|
||||
static applyTransformsToBody = ({fragment}) => {
|
||||
const root = fragment.childNodes[0];
|
||||
root.innerHTML = root.innerHTML.replace(/<\/?code[^>]*>/g, (match) =>
|
||||
`<!-- ${match} -->`
|
||||
);
|
||||
return nextDraft;
|
||||
}
|
||||
|
||||
static unapplyTransformsToDraft = ({draft}) => {
|
||||
const nextDraft = draft.clone();
|
||||
nextDraft.body = nextDraft.body.replace(/<!-- (<\/?code[^>]*>) -->/g, (match, node) =>
|
||||
static unapplyTransformsToBody = ({fragment}) => {
|
||||
const root = fragment.childNodes[0];
|
||||
root.innerHTML = root.innerHTML.replace(/<!-- (<\/?code[^>]*>) -->/g, (match, node) =>
|
||||
node
|
||||
);
|
||||
return nextDraft;
|
||||
}
|
||||
|
||||
static onClick({editor, event}) {
|
||||
|
|
|
@ -180,7 +180,7 @@ describe "ComposerView", ->
|
|||
|
||||
describe "empty body warning", ->
|
||||
it "warns if the body of the email is still the pristine body", ->
|
||||
pristineBody = "<head></head><body><br><br></body>"
|
||||
pristineBody = "<br><br>"
|
||||
|
||||
useDraft.call @,
|
||||
to: [u1]
|
||||
|
|
|
@ -1,88 +1,37 @@
|
|||
import React from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import ComposerExtension from '../../extensions/composer-extension'
|
||||
// import {ANCHOR_CLASS, IMG_SRC} from './anchor-constants'
|
||||
import OverlaidComponents from './overlaid-components'
|
||||
import CustomContenteditableComponents from './custom-contenteditable-components'
|
||||
|
||||
export default class OverlaidComposerExtension extends ComposerExtension {
|
||||
|
||||
// https://regex101.com/r/fW6sV3/2
|
||||
static _serializedExtractRe() {
|
||||
return /<overlay .*?data-overlay-id="(.*?)" data-component-props="(.*?)" data-component-key="(.*?)" data-style="(.*?)".*?>.*?<\/overlay>/gmi
|
||||
}
|
||||
|
||||
static _serializedReplacerRe(id) {
|
||||
return new RegExp(`<overlay .*?data-overlay-id="${id}".*?>.*?<\/overlay>`, 'gim')
|
||||
}
|
||||
|
||||
// https://regex101.com/r/rK3uA3/1
|
||||
static _anchorExtractRe() {
|
||||
return /<img .*?data-overlay-id="(.*?)" data-component-props="(.*?)" data-component-key="(.*?)" style="(.*?)".*?>/gmi
|
||||
}
|
||||
|
||||
static _anchorReplacerRe(id) {
|
||||
return new RegExp(`<img .*?data-overlay-id="${id}".*?>`, 'gim')
|
||||
}
|
||||
|
||||
static *overlayMatches(re, body) {
|
||||
let result = re.exec(body);
|
||||
while (result) {
|
||||
let props = result[2];
|
||||
props = JSON.parse(props.replace(/"/g, `"`));
|
||||
const data = {
|
||||
dataOverlayId: result[1],
|
||||
dataComponentProps: props,
|
||||
dataComponentKey: result[3],
|
||||
dataStyle: result[4],
|
||||
static applyTransformsToBody({fragment, draft}) {
|
||||
const overlayImgEls = Array.from(fragment.querySelector('img[data-overlay-id]'));
|
||||
for (const imgEl of overlayImgEls) {
|
||||
const Component = CustomContenteditableComponents.get(imgEl.dataset.componentKey);
|
||||
if (!Component) {
|
||||
continue;
|
||||
}
|
||||
yield data
|
||||
result = re.exec(body);
|
||||
|
||||
const props = Object.assign({draft, isPreview: true}, imgEl.dataset.componentProps);
|
||||
const reactElement = React.createElement(Component, props);
|
||||
|
||||
const overlayEl = document.createElement('overlay');
|
||||
overlayEl.innerHTML = ReactDOMServer.renderToStaticMarkup(reactElement);
|
||||
Object.assign(overlayEl.dataset, imgEl.dataset);
|
||||
|
||||
imgEl.parentNode.replaceChild(overlayEl, imgEl);
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
static applyTransformsToDraft({draft}) {
|
||||
const self = OverlaidComposerExtension;
|
||||
const outDraft = draft.clone();
|
||||
let outBody = outDraft.body;
|
||||
const matcher = self.overlayMatches(self._anchorExtractRe(), outDraft.body)
|
||||
|
||||
for (const match of matcher) {
|
||||
const component = CustomContenteditableComponents.get(match.dataComponentKey);
|
||||
if (!component) {
|
||||
continue
|
||||
}
|
||||
const props = Object.assign({draft, isPreview: true}, match.dataComponentProps);
|
||||
const el = React.createElement(component, props);
|
||||
let html = ReactDOMServer.renderToStaticMarkup(el);
|
||||
|
||||
html = `<overlay data-overlay-id="${match.dataOverlayId}" data-component-props="${OverlaidComponents.propsToDOMAttr(match.dataComponentProps)}" data-component-key="${match.dataComponentKey}" data-style="${match.dataStyle}">${html}</overlay>`
|
||||
|
||||
outBody = outBody.replace(
|
||||
OverlaidComposerExtension._anchorReplacerRe(match.dataOverlayId),
|
||||
html
|
||||
)
|
||||
static unapplyTransformsToDraft({fragment}) {
|
||||
const overlayEls = Array.from(fragment.querySelector('overlay[data-overlay-id]'));
|
||||
for (const overlayEl of overlayEls) {
|
||||
const {componentKey, componentProps, overlayId, style} = overlayEl.dataset;
|
||||
const {anchorTag} = OverlaidComponents.buildAnchorTag(componentKey, componentProps, overlayId, style);
|
||||
const anchorFragment = document.createRange().createContextualFragment(anchorTag);
|
||||
overlayEl.parentNode.replaceChild(anchorFragment, overlayEl);
|
||||
}
|
||||
|
||||
outDraft.body = outBody;
|
||||
return outDraft;
|
||||
}
|
||||
|
||||
static unapplyTransformsToDraft({draft}) {
|
||||
const self = OverlaidComposerExtension;
|
||||
const outDraft = draft.clone();
|
||||
let outBody = outDraft.body
|
||||
|
||||
const matcher = self.overlayMatches(self._serializedExtractRe(), outDraft.body);
|
||||
|
||||
for (const match of matcher) {
|
||||
const {anchorTag} = OverlaidComponents.buildAnchorTag(match.dataComponentKey, match.dataComponentProps, match.dataOverlayId, match.dataStyle);
|
||||
|
||||
outBody = outBody.replace(OverlaidComposerExtension._serializedReplacerRe(match.dataOverlayId), anchorTag)
|
||||
}
|
||||
|
||||
outDraft.body = outBody;
|
||||
return outDraft;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,15 +142,15 @@ class ComposerExtension extends ContenteditableExtension
|
|||
|
||||
- `draft`: A {Message} the user is about to finish editing.
|
||||
###
|
||||
@applyTransformsToDraft: ({draft}) ->
|
||||
return draft
|
||||
@applyTransformsToBody: ({draft, fragment}) ->
|
||||
return
|
||||
|
||||
###
|
||||
Public: unapplyTransformsToDraft should revert the changes made in
|
||||
`applyTransformsToDraft`. See the documentation for that method for more
|
||||
information.
|
||||
###
|
||||
@unapplyTransformsToDraft: ({draft}) ->
|
||||
return draft
|
||||
@unapplyTransformsToBody: ({draft, fragment}) ->
|
||||
return
|
||||
|
||||
module.exports = ComposerExtension
|
||||
|
|
|
@ -119,6 +119,9 @@ export function applyExtensionTransformsToBody(draft) {
|
|||
const range = document.createRange();
|
||||
const fragment = range.createContextualFragment(`<root>${draft.body}</root>`);
|
||||
const extensions = ExtensionRegistry.Composer.extensions();
|
||||
console.log('--BEFORE------------------------------');
|
||||
console.log(draft.body);
|
||||
console.log('--------------------------------');
|
||||
|
||||
return Promise.each(extensions, (ext) => {
|
||||
const extApply = ext.applyTransformsToBody;
|
||||
|
@ -133,23 +136,30 @@ export function applyExtensionTransformsToBody(draft) {
|
|||
});
|
||||
}).then(() => {
|
||||
draft.body = fragment.childNodes[0].innerHTML;
|
||||
console.log('--AFTER------------------------------');
|
||||
console.log(draft.body);
|
||||
console.log('--------------------------------');
|
||||
return draft;
|
||||
});
|
||||
}
|
||||
|
||||
export function prepareDraftForSyncback(session) {
|
||||
return session.ensureCorrectAccount({noSyncback: true})
|
||||
.then(() => applyExtensionTransformsToDraft(session.draft()))
|
||||
.then((transformed) => applyExtensionTransformsToBody(transformed))
|
||||
.then(() =>
|
||||
applyExtensionTransformsToDraft(session.draft()))
|
||||
.then((transformed) =>
|
||||
applyExtensionTransformsToBody(transformed))
|
||||
.then((transformed) => {
|
||||
if (!transformed.replyToMessageId || !shouldAppendQuotedText(transformed)) {
|
||||
return Promise.resolve(transformed)
|
||||
return Promise.resolve(transformed);
|
||||
}
|
||||
return appendQuotedTextToDraft(transformed)
|
||||
return appendQuotedTextToDraft(transformed);
|
||||
})
|
||||
.then((draft) => (
|
||||
DatabaseStore.inTransaction((t) => t.persistModel(draft))
|
||||
.then(() => Promise.resolve(queueDraftFileUploads(draft)))
|
||||
.thenReturn(draft)
|
||||
DatabaseStore.inTransaction((t) =>
|
||||
t.persistModel(draft)
|
||||
).then(() =>
|
||||
Promise.resolve(queueDraftFileUploads(draft))
|
||||
).thenReturn(draft)
|
||||
))
|
||||
}
|
||||
|
|
2
src/pro
2
src/pro
|
@ -1 +1 @@
|
|||
Subproject commit 45756b80645edcedfa7d015dcff8a26bcb521ec0
|
||||
Subproject commit 3938f5e5834df59da5e9a4a1b6073ba57de51fb0
|
Loading…
Reference in a new issue