mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
fix(mail-merge): Switch to using new overlaid components api
Summary: - Fixes several selection and focus issues along the way - Can now preview what tokens will look like when not editing - Adds decorator to listen to mail merge session changes and removes a bunch of duplicated code - Gets rid of all imperative code (and specs) that had to imperatively reach into the dom to update the tokens Test Plan: - Unit tests Reviewers: bengotow, evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D2989
This commit is contained in:
parent
6ba667daa2
commit
3398640e86
6 changed files with 44 additions and 15 deletions
|
@ -460,7 +460,6 @@ body.platform-win32 {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
img.n1-overlaid-component-anchor-container {
|
img.n1-overlaid-component-anchor-container {
|
||||||
display: block;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
.toggle-serialized {
|
.toggle-serialized {
|
||||||
|
|
|
@ -42,6 +42,8 @@ render: function() {
|
||||||
class Contenteditable extends React.Component
|
class Contenteditable extends React.Component
|
||||||
@displayName: "Contenteditable"
|
@displayName: "Contenteditable"
|
||||||
|
|
||||||
|
@IgnoreMutationClassName: 'ignore-mutations'
|
||||||
|
|
||||||
@propTypes:
|
@propTypes:
|
||||||
# The current html state, as a string, of the contenteditable.
|
# The current html state, as a string, of the contenteditable.
|
||||||
value: React.PropTypes.string
|
value: React.PropTypes.string
|
||||||
|
@ -298,16 +300,27 @@ class Contenteditable extends React.Component
|
||||||
########################### Event Handlers ###########################
|
########################### Event Handlers ###########################
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
# Every time the contents of the contenteditable DOM node change, the
|
# Every time the contents of the contenteditable DOM node change due to a user
|
||||||
# `_onDOMMutated` event gets fired.
|
# action, the `_onDOMMutated` event gets fired.
|
||||||
#
|
#
|
||||||
# If we are in the middle of an `atomic` change transaction, we ignore
|
# If we are in the middle of an `atomic` change transaction, we ignore
|
||||||
# those changes.
|
# those changes.
|
||||||
#
|
#
|
||||||
|
# If any target element of the mutations contains `IgnoreMutationClassName` in
|
||||||
|
# its className, we will also ignore the mutations. This is order to support
|
||||||
|
# extensions that might imperatively want to mutate the contenteditable
|
||||||
|
# wihtout it being due to a direct user action, i.e. without wanting to
|
||||||
|
# trigger this callback. For example, `OverlaidComponents` will mutate the
|
||||||
|
# dimensions of its anchor elements without the user explicitly doing so.
|
||||||
|
#
|
||||||
# At all other times we take the change, apply various filters to the
|
# At all other times we take the change, apply various filters to the
|
||||||
# new content, then notify our parent that the content has been updated.
|
# new content, then notify our parent that the content has been updated.
|
||||||
|
#
|
||||||
_onDOMMutated: (mutations) =>
|
_onDOMMutated: (mutations) =>
|
||||||
return unless mutations and mutations.length > 0
|
return unless mutations and mutations.length > 0
|
||||||
|
for mutation in mutations
|
||||||
|
if mutation.target?.className?.includes(Contenteditable.IgnoreMutationClassName)
|
||||||
|
return
|
||||||
|
|
||||||
@_mutationObserver.disconnect()
|
@_mutationObserver.disconnect()
|
||||||
@setInnerState dragging: false if @innerState.dragging
|
@setInnerState dragging: false if @innerState.dragging
|
||||||
|
|
|
@ -2,7 +2,7 @@ _ = require 'underscore'
|
||||||
{DOMUtils} = require 'nylas-exports'
|
{DOMUtils} = require 'nylas-exports'
|
||||||
React = require 'react'
|
React = require 'react'
|
||||||
ExtendedSelection = require './extended-selection'
|
ExtendedSelection = require './extended-selection'
|
||||||
OverlaidComponents = require('../overlaid-components/overlaid-components').default
|
OverlaidComponents = null
|
||||||
|
|
||||||
# An extended interface of execCommand
|
# An extended interface of execCommand
|
||||||
#
|
#
|
||||||
|
@ -73,8 +73,15 @@ class EditorAPI
|
||||||
normalize: -> @rootNode.normalize(); @
|
normalize: -> @rootNode.normalize(); @
|
||||||
|
|
||||||
insertCustomComponent: (componentKey, props = {}) ->
|
insertCustomComponent: (componentKey, props = {}) ->
|
||||||
anchorTag = OverlaidComponents.buildAnchorTag(componentKey, props)
|
OverlaidComponents ?= require('../overlaid-components/overlaid-components').default
|
||||||
|
{anchorId, anchorTag} = OverlaidComponents.buildAnchorTag(componentKey, props)
|
||||||
@insertHTML(anchorTag)
|
@insertHTML(anchorTag)
|
||||||
|
return anchorId
|
||||||
|
|
||||||
|
removeCustomComponentByAnchorId: (anchorId) ->
|
||||||
|
return unless anchorId
|
||||||
|
node = @rootNode.querySelector("img[data-overlay-id=\"#{anchorId}\"]")
|
||||||
|
node?.parentNode.removeChild(node)
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
####################### execCommand Delegation #########################
|
####################### execCommand Delegation #########################
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import Utils from '../../flux/models/utils'
|
import Utils from '../../flux/models/utils'
|
||||||
import CustomContenteditableComponents from './custom-contenteditable-components'
|
import CustomContenteditableComponents from './custom-contenteditable-components'
|
||||||
|
import {IgnoreMutationClassName} from '../contenteditable/contenteditable'
|
||||||
import {ANCHOR_CLASS, IMG_SRC} from './anchor-constants'
|
import {ANCHOR_CLASS, IMG_SRC} from './anchor-constants'
|
||||||
|
|
||||||
const MUTATION_CONFIG = {
|
const MUTATION_CONFIG = {
|
||||||
|
@ -19,6 +20,7 @@ export default class OverlaidComponents extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: React.PropTypes.node,
|
children: React.PropTypes.node,
|
||||||
|
className: React.PropTypes.string,
|
||||||
exposedProps: React.PropTypes.object,
|
exposedProps: React.PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,11 +36,17 @@ export default class OverlaidComponents extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
static buildAnchorTag(componentKey, props = {}, existingId = null, style = "") {
|
static buildAnchorTag(componentKey, props = {}, existingId = null, style = "") {
|
||||||
const id = existingId || Utils.generateTempId()
|
const overlayId = existingId || Utils.generateTempId()
|
||||||
let className = ANCHOR_CLASS
|
let className = `${IgnoreMutationClassName} ${ANCHOR_CLASS}`
|
||||||
if (props.className) { className = `${className} ${props.className}` }
|
if (props.className) {
|
||||||
|
className = `${className} ${props.className}`
|
||||||
|
}
|
||||||
const propsStr = OverlaidComponents.propsToDOMAttr(props);
|
const propsStr = OverlaidComponents.propsToDOMAttr(props);
|
||||||
return `<img class="${className}" src="${IMG_SRC}" data-overlay-id="${id}" data-component-props="${propsStr}" data-component-key="${componentKey}" style="${style}">`
|
return {
|
||||||
|
anchorId: overlayId,
|
||||||
|
anchorTag:
|
||||||
|
`<img class="${className}" src="${IMG_SRC}" data-overlay-id="${overlayId}" data-component-props="${propsStr}" data-component-key="${componentKey}" style="${style}">`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -168,7 +176,7 @@ export default class OverlaidComponents extends React.Component {
|
||||||
const data = this._anchorData[id];
|
const data = this._anchorData[id];
|
||||||
if (!data) { throw new Error("No mounted rect for #{id}") }
|
if (!data) { throw new Error("No mounted rect for #{id}") }
|
||||||
|
|
||||||
const style = {left: data.left, top: data.top, position: "relative"}
|
const style = {left: data.left, top: data.top, position: 'absolute'}
|
||||||
const componentData = CustomContenteditableComponents.get(data.componentKey);
|
const componentData = CustomContenteditableComponents.get(data.componentKey);
|
||||||
|
|
||||||
if (!componentData) {
|
if (!componentData) {
|
||||||
|
@ -189,13 +197,14 @@ export default class OverlaidComponents extends React.Component {
|
||||||
const el = React.createElement(component, props)
|
const el = React.createElement(component, props)
|
||||||
|
|
||||||
const wrap = (
|
const wrap = (
|
||||||
<div
|
<span
|
||||||
|
key={id}
|
||||||
className={OverlaidComponents.WRAP_CLASS}
|
className={OverlaidComponents.WRAP_CLASS}
|
||||||
style={style}
|
style={style}
|
||||||
data-overlay-id={id}
|
data-overlay-id={id}
|
||||||
>
|
>
|
||||||
{el}
|
{el}
|
||||||
</div>
|
</span>
|
||||||
)
|
)
|
||||||
|
|
||||||
els.push(wrap)
|
els.push(wrap)
|
||||||
|
@ -210,8 +219,9 @@ export default class OverlaidComponents extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {className} = this.props
|
||||||
return (
|
return (
|
||||||
<div className="overlaid-components-wrap" style={{position: "relative"}}>
|
<div className={`overlaid-components-wrap ${className}`} style={{position: "relative"}}>
|
||||||
<div className="anchor-container" ref="anchorContainer">
|
<div className="anchor-container" ref="anchorContainer">
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -75,7 +75,7 @@ export default class OverlaidComposerExtension extends ComposerExtension {
|
||||||
const matcher = self.overlayMatches(self._serializedExtractRe(), outDraft.body);
|
const matcher = self.overlayMatches(self._serializedExtractRe(), outDraft.body);
|
||||||
|
|
||||||
for (const match of matcher) {
|
for (const match of matcher) {
|
||||||
const anchorTag = OverlaidComponents.buildAnchorTag(match.dataComponentKey, match.dataComponentProps, match.dataOverlayId, match.dataStyle);
|
const {anchorTag} = OverlaidComponents.buildAnchorTag(match.dataComponentKey, match.dataComponentProps, match.dataOverlayId, match.dataStyle);
|
||||||
|
|
||||||
outBody = outBody.replace(OverlaidComposerExtension._serializedReplacerRe(match.dataOverlayId), anchorTag)
|
outBody = outBody.replace(OverlaidComposerExtension._serializedReplacerRe(match.dataOverlayId), anchorTag)
|
||||||
}
|
}
|
||||||
|
|
2
src/pro
2
src/pro
|
@ -1 +1 @@
|
||||||
Subproject commit 8fcfe13577d014ad4583b5f23b639047f1cb5789
|
Subproject commit b0d3088fd8d9f853d82872ddd5aae14cc02447cf
|
Loading…
Reference in a new issue