From ee49db6a09c314d448d0cc32d348d5190dfd76bb Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Thu, 22 Sep 2016 17:36:06 -0700 Subject: [PATCH] WIP --- docs/ComposerExtensions.md | 11 +-- .../lib/markdown-composer-extension.coffee | 18 ++-- .../lib/spellcheck-composer-extension.es6 | 18 +++- .../spellcheck-composer-extension-spec.es6 | 24 ++--- .../lib/template-composer-extension.es6 | 14 ++- .../composer/spec/composer-view-spec.cjsx | 2 +- .../overlaid-composer-extension.es6 | 95 +++++-------------- src/extensions/composer-extension.coffee | 8 +- src/flux/stores/draft-helpers.es6 | 24 +++-- src/pro | 2 +- 10 files changed, 85 insertions(+), 131 deletions(-) diff --git a/docs/ComposerExtensions.md b/docs/ComposerExtensions.md index 7ef6d625e..c607fbb4b 100644 --- a/docs/ComposerExtensions.md +++ b/docs/ComposerExtensions.md @@ -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 += "
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) ``` diff --git a/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee b/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee index f08f00a4a..77245969c 100644 --- a/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee +++ b/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee @@ -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 diff --git a/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6 b/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6 index 197bf9dc4..8b1bb6443 100644 --- a/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6 +++ b/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6 @@ -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! + } } diff --git a/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6 b/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6 index 79041afcb..7023d0ceb 100644 --- a/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6 +++ b/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6 @@ -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(`${afterHTML}`); + SpellcheckComposerExtension.applyTransformsToBody({fragment, draft}); + expect(fragment.childNodes[0].innerHTML).toEqual(initialHTML); }); }); }); diff --git a/internal_packages/composer-templates/lib/template-composer-extension.es6 b/internal_packages/composer-templates/lib/template-composer-extension.es6 index fad672444..e1bc5ed41 100644 --- a/internal_packages/composer-templates/lib/template-composer-extension.es6 +++ b/internal_packages/composer-templates/lib/template-composer-extension.es6 @@ -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) => `` ); - return nextDraft; } - static unapplyTransformsToDraft = ({draft}) => { - const nextDraft = draft.clone(); - nextDraft.body = nextDraft.body.replace(//g, (match, node) => + static unapplyTransformsToBody = ({fragment}) => { + const root = fragment.childNodes[0]; + root.innerHTML = root.innerHTML.replace(//g, (match, node) => node ); - return nextDraft; } static onClick({editor, event}) { diff --git a/internal_packages/composer/spec/composer-view-spec.cjsx b/internal_packages/composer/spec/composer-view-spec.cjsx index cebd6b6aa..7a3a12778 100644 --- a/internal_packages/composer/spec/composer-view-spec.cjsx +++ b/internal_packages/composer/spec/composer-view-spec.cjsx @@ -180,7 +180,7 @@ describe "ComposerView", -> describe "empty body warning", -> it "warns if the body of the email is still the pristine body", -> - pristineBody = "

" + pristineBody = "

" useDraft.call @, to: [u1] diff --git a/src/components/overlaid-components/overlaid-composer-extension.es6 b/src/components/overlaid-components/overlaid-composer-extension.es6 index e32c3b8a2..7b4ac935d 100644 --- a/src/components/overlaid-components/overlaid-composer-extension.es6 +++ b/src/components/overlaid-components/overlaid-composer-extension.es6 @@ -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>/gmi - } - - static _serializedReplacerRe(id) { - return new RegExp(`.*?<\/overlay>`, 'gim') - } - - // https://regex101.com/r/rK3uA3/1 - static _anchorExtractRe() { - return //gmi - } - - static _anchorReplacerRe(id) { - return new RegExp(``, '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 = `${html}` - - 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; } } diff --git a/src/extensions/composer-extension.coffee b/src/extensions/composer-extension.coffee index 7af5b0c92..a68e4ec92 100644 --- a/src/extensions/composer-extension.coffee +++ b/src/extensions/composer-extension.coffee @@ -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 diff --git a/src/flux/stores/draft-helpers.es6 b/src/flux/stores/draft-helpers.es6 index ad386a72b..559cb7e7e 100644 --- a/src/flux/stores/draft-helpers.es6 +++ b/src/flux/stores/draft-helpers.es6 @@ -119,6 +119,9 @@ export function applyExtensionTransformsToBody(draft) { const range = document.createRange(); const fragment = range.createContextualFragment(`${draft.body}`); 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) )) } diff --git a/src/pro b/src/pro index 45756b806..3938f5e58 160000 --- a/src/pro +++ b/src/pro @@ -1 +1 @@ -Subproject commit 45756b80645edcedfa7d015dcff8a26bcb521ec0 +Subproject commit 3938f5e5834df59da5e9a4a1b6073ba57de51fb0