substance
Version:
Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing system. It is developed to power our online editing platform [Substance](http://substance.io).
278 lines (246 loc) • 7.96 kB
JavaScript
import { debounce, keys, platform } from '../util'
import { Component, $$ } from '../dom'
const UPDATE_DELAY = 300
export default class FindAndReplaceDialog extends Component {
constructor (...args) {
super(...args)
// debounce updates when patterns change, but not during tests
if (!platform.test) {
this._updatePattern = debounce(this._updatePattern.bind(this), UPDATE_DELAY)
this._updateReplacePattern = debounce(this._updateReplacePattern.bind(this), UPDATE_DELAY)
}
}
didMount () {
this.context.editorState.addObserver(['findAndReplace'], this._onUpdate, this, { stage: 'render' })
}
dispose () {
this.context.editorState.removeObserver(this)
}
render () {
const state = this._getState()
const el = $$('div').addClass('sc-find-and-replace-dialog')
el.append(
this._renderHeader(),
this._renderFindSection(),
this._renderReplaceSection()
)
if (!state.enabled) {
el.addClass('sm-hidden')
}
el.on('keydown', this._onKeydown)
return el
}
_renderTitle () {
const state = this._getState()
let title = state.showReplace ? this.getLabel(`find-replace-title-${this.props.viewName}`) : this.getLabel(`find-title-${this.props.viewName}`)
const options = []
if (state.caseSensitive) options.push('case-sensitive-title')
if (state.fullWord) options.push('whole-word-title')
if (state.regexSearch) options.push('regex-title')
if (options.length > 0) title += ' (' + options.map(o => this.getLabel(o)).join(', ') + ')'
return $$('div').addClass('se-title').append(title)
}
_renderHeader () {
const state = this._getState()
const Button = this.getComponent('button')
return $$('div').addClass('se-header').append(
this._renderTitle(),
$$('div').addClass('se-group sm-options').append(
$$(Button, {
tooltip: this.getLabel('find-case-sensitive'),
active: state.caseSensitive,
theme: this.props.theme
}).addClass('sm-case-sensitive').append('Aa')
.on('click', this._toggleCaseSensitivity),
$$(Button, {
tooltip: this.getLabel('find-whole-word'),
active: state.fullWord,
theme: this.props.theme
}).addClass('sm-whole-word').append('Abc|')
.on('click', this._toggleFullWordSearch),
$$(Button, {
tooltip: this.getLabel('find-regex'),
active: state.regexSearch,
theme: this.props.theme
}).addClass('sm-regex-search').append('.*')
.on('click', this._toggleRegexSearch),
$$(Button, {
tooltip: this.getLabel('close'),
theme: this.props.theme
}).addClass('sm-close')
.append(
this.context.iconProvider.renderIcon('close')
)
.on('click', this._close)
)
)
}
_renderFindSection () {
const state = this._getState()
const Button = this.getComponent('button')
return $$('div').addClass('se-section').addClass('sm-find').append(
$$('div').addClass('se-group sm-input').append(
this._renderPatternInput(),
this._renderStatus()
),
$$('div').addClass('se-group sm-actions').append(
$$(Button, {
tooltip: this.getLabel('find-next'),
theme: this.props.theme,
disabled: state.count < 1
}).addClass('sm-next')
.append(this.getLabel('next'))
.on('click', this._findNext),
$$(Button, {
tooltip: this.getLabel('find-previous'),
theme: this.props.theme,
disabled: state.count < 1
}).addClass('sm-previous')
.append(this.getLabel('previous'))
.on('click', this._findPrevious)
)
)
}
_renderReplaceSection () {
const state = this._getState()
if (state.showReplace) {
const Button = this.getComponent('button')
return $$('div').addClass('se-section').addClass('sm-replace').append(
$$('div').addClass('se-group sm-input').append(
this._renderReplacePatternInput()
),
$$('div').addClass('se-group sm-actions').append(
$$(Button, {
tooltip: this.getLabel('replace'),
theme: this.props.theme,
disabled: state.count < 1
}).addClass('sm-replace')
.append(this.getLabel('replace'))
.on('click', this._replaceNext),
$$(Button, {
tooltip: this.getLabel('replace-all'),
theme: this.props.theme,
disabled: state.count < 1
}).addClass('sm-replace-all')
.append(this.getLabel('replace-all'))
.on('click', this._replaceAll)
)
)
}
}
_renderPatternInput () {
const state = this._getState()
return $$('input').ref('pattern').addClass('sm-find')
.attr({
type: 'text',
placeholder: this.getLabel('find'),
tabindex: 500
})
.val(state.pattern)
.on('keydown', this._onPatternKeydown)
.on('input', this._updatePattern)
.on('focus', this._onFocus)
}
_renderReplacePatternInput () {
const state = this._getState()
return $$('input').ref('replacePattern').addClass('sm-replace')
.attr({
type: 'text',
placeholder: this.getLabel('replace'),
tabindex: 500
})
.val(state.replacePattern)
.on('keydown', this._onReplacePatternKeydown)
.on('input', this._updateReplacePattern)
}
_renderStatus () {
const state = this._getState()
const el = $$('span').addClass('se-status')
if (state.count > 0) {
const current = state.cursor === -1 ? '?' : String(state.cursor + 1)
el.append(`${current} of ${state.count}`)
} else if (state.pattern) {
el.append(this.getLabel('no-result'))
}
return el
}
_grabFocus () {
const state = this._getState()
const input = state.showReplace ? this.refs.replacePattern : this.refs.pattern
input.el.focus()
}
_getState () {
const editorState = this.context.editorSession.getEditorState()
return editorState.findAndReplace
}
_getManager () {
return this.context.findAndReplaceManager
}
_close () {
this._getManager().closeDialog()
}
_findNext () {
this._getManager().next()
}
_findPrevious () {
this._getManager().previous()
}
_replaceNext () {
this._getManager().replaceNext()
}
_replaceAll () {
this._getManager().replaceAll()
}
_updatePattern () {
// console.log('FindAndReplaceDialog._updatePattern()', this.refs.pattern.val())
this._getManager().setSearchPattern(this.refs.pattern.val())
}
_updateReplacePattern () {
this._getManager().setReplacePattern(this.refs.replacePattern.val())
}
_toggleCaseSensitivity () {
this._getManager().toggleCaseSensitivity()
}
_toggleFullWordSearch () {
this._getManager().toggleFullWordSearch()
}
_toggleRegexSearch () {
this._getManager().toggleRegexSearch()
}
_onUpdate () {
// if this dialog is made visible, auto-focus the respective pattern input field
// TODO: maybe we should let the app control this
const wasHidden = this.el.hasClass('sm-hidden')
this.rerender()
const isHidden = this.el.hasClass('sm-hidden')
if (wasHidden && !isHidden) {
this._grabFocus()
}
}
_onKeydown (e) {
if (e.keyCode === keys.ESCAPE) {
e.stopPropagation()
e.preventDefault()
this._close()
}
}
_onFocus (e) {
e.stopPropagation()
// TODO: should this be propagated?
this.context.editorState.isBlurred = true
}
_onPatternKeydown (e) {
e.stopPropagation()
if (e.keyCode === keys.ENTER) {
e.preventDefault()
this._findNext()
}
}
_onReplacePatternKeydown (e) {
e.stopPropagation()
if (e.keyCode === keys.ENTER) {
e.preventDefault()
this._replaceNext()
}
}
}