rebuild/src/main/resources/web/assets/js/rb-components.js

1517 lines
44 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*!
Copyright (c) REBUILD <https://getrebuild.com/> and/or its owners. All rights reserved.
rebuild is dual-licensed under commercial and open source licenses (GPLv3).
See LICENSE and COMMERCIAL in the project root for license information.
*/
/* eslint-disable no-unused-vars */
/* global EasyMDE */
// ~~ Modal 兼容子元素和 iFrame
class RbModal extends React.Component {
constructor(props) {
super(props)
this.state = { ...props }
}
render() {
const props = this.props
const style2 = { maxWidth: ~~(props.width || 680) }
if (props.useWhite || props.maximize) {
style2.maxWidth = this.state._maximize ? $(window).width() - 60 : null
if (!style2.maxWidth && props.width) style2.maxWidth = ~~props.width
}
const modalClazz = props.useWhite ? 'modal rbmodal use-white' : `modal rbmodal colored-header colored-header-${props.colored || 'primary'}`
return (
<div
className={modalClazz}
style={props.zIndex ? { zIndex: props.zIndex } : null}
aria-modal="true"
tabIndex="-1"
ref={(c) => {
this._rbmodal = c
this._element = c
}}>
<div className={`modal-dialog ${props.useWhite && 'modal-xl'} ${props.className || ''}`} style={style2}>
<div className="modal-content" style={style2}>
<div
className={`modal-header ${props.useWhite ? '' : 'modal-header-colored'}`}
onDoubleClick={(e) => {
if (this.props.maximize) {
$stopEvent(e, true)
this.setState({ _maximize: !this.state._maximize })
}
}}>
{props.icon && <i className={`icon zmdi zmdi-${props.icon}`} />}
<h3 className="modal-title">{props.title || ''}</h3>
{props.url && props.urlOpenInNew && (
<a className="close s fs-18" href={props.url} target="_blank" title={$L('在新页面打开')}>
<span className="zmdi zmdi-open-in-new" />
</a>
)}
{this.props.maximize && (
<button
className="close md-close J_maximize"
type="button"
title={this.state._maximize ? $L('向下还原') : $L('最大化')}
onClick={() => this.setState({ _maximize: !this.state._maximize })}
style={{ marginTop: -9 }}>
<span className={`mdi ${this.state._maximize ? 'mdi mdi-window-restore' : 'mdi mdi-window-maximize'}`} />
</button>
)}
<button className="close" type="button" onClick={() => this.hide()} title={`${$L('关闭')} (Esc)`}>
<span className="zmdi zmdi-close" />
</button>
</div>
{this.renderContent()}
</div>
</div>
</div>
)
}
renderContent() {
const iframe = !this.props.children // No child
return (
<div className={`modal-body ${iframe ? 'iframe rb-loading' : ''} ${iframe && this.state.frameLoad !== false ? 'rb-loading-active' : ''}`}>
{this.props.children || <iframe src={this.props.url} frameBorder="0" scrolling="no" onLoad={() => this.resize()} />}
{iframe && <RbSpinner />}
</div>
)
}
componentDidMount() {
const $root = $(this._rbmodal)
.modal({
show: true,
backdrop: this.props.backdrop === false ? false : 'static',
keyboard: true,
})
.on('hidden.bs.modal', () => {
$keepModalOpen()
if (this.props.disposeOnHide === true) {
$root.modal('dispose')
$unmount($root.parent(), 0, null, this.props.__root18)
}
})
}
show() {
$(this._rbmodal).modal('show')
typeof this.props.onShow === 'function' && this.props.onShow(this)
}
hide() {
$(this._rbmodal).modal('hide')
typeof this.props.onHide === 'function' && this.props.onHide(this)
}
resize() {
if (this.props.children) return
const $root = $(this._rbmodal)
$setTimeout(
() => {
const $iframe = $root.find('iframe')
let height = $iframe.contents().find('.main-content').outerHeight()
if (height === 0) height = $iframe.contents().find('body').height()
// else height += 45 // .main-content's padding
$root.find('.modal-body').height(height)
this.setState({ frameLoad: false })
},
20,
'RbModal-resize'
)
}
// -- Usage
/**
* @param {*} url
* @param {*} title
* @param {*} option
*/
static create(url, title, option) {
// URL prefix
if (url.substr(0, 1) === '/' && rb.baseUrl) url = rb.baseUrl + url
option = option || {}
option.disposeOnHide = option.disposeOnHide === true // default false
this.__HOLDERs = this.__HOLDERs || {}
const that = this
if (option.disposeOnHide === false && !!that.__HOLDERs[url]) {
that.__HOLDER = that.__HOLDERs[url]
that.__HOLDER.show()
that.__HOLDER.resize()
} else {
renderRbcomp(<RbModal url={url} urlOpenInNew={option.urlOpenInNew} title={title} width={option.width} disposeOnHide={option.disposeOnHide} zIndex={option.zIndex} />, function () {
that.__HOLDER = this
if (option.disposeOnHide === false) that.__HOLDERs[url] = this
})
}
}
/**
* @param {*} url
*/
static hide(url) {
this.__HOLDERs = this.__HOLDERs || {}
if (url) {
const found = this.__HOLDERs[rb.baseUrl + url] || this.__HOLDERs[url]
found && found.hide()
} else if (this.__HOLDER) {
this.__HOLDER.hide()
}
}
/**
* @param {*} url
*/
static resize(url) {
this.__HOLDERs = this.__HOLDERs || {}
if (url) this.__HOLDERs[url] && this.__HOLDERs[url].resize()
else if (this.__HOLDER) this.__HOLDER.resize()
}
}
// ~~ Modal 处理器
class RbModalHandler extends React.Component {
constructor(props) {
super(props)
this.state = { ...props }
}
show = (state, cb) => {
const callback = () => {
// eslint-disable-next-line react/no-string-refs
const dlg = this._dlg || this.refs['dlg']
if (dlg) dlg.show()
typeof cb === 'function' && cb(this)
}
if (state && $type(state) === 'object') this.setState(state, callback)
else callback()
}
hide = (e) => {
if (e && e.target && $(e.target).attr('disabled')) return
// eslint-disable-next-line react/no-string-refs
const dlg = this._dlg || this.refs['dlg']
if (dlg) dlg.hide()
}
}
// ~~ FormModal 处理器
class RbFormHandler extends RbModalHandler {
constructor(props) {
super(props)
this.state = { ...props }
}
handleChange = (e, call) => {
const target = e.target
const name = target.dataset.id || target.name
if (!name) return
const val = target.type === 'checkbox' ? target.checked : target.value
const s = {}
s[name] = val
this.setState(s, call)
this.handleChangeAfter(name, val)
}
handleChangeAfter(name, value) {
// NOOP
}
componentWillUnmount() {
if (this.__select2) {
__destroySelect2(this.__select2)
this.__select2 = null
}
}
disabled(d, preventHide) {
this._dlg && _preventHide(d, preventHide, this._dlg._element)
if (d === true) $(this._btns).find('.btn').button('loading')
else $(this._btns).find('.btn').button('reset')
}
}
// ~~ 提示框
class RbAlert extends React.Component {
constructor(props) {
super(props)
this.state = { ...props }
}
render() {
const style1 = {}
const style2 = {}
if (this.props.zIndex) style1.zIndex = this.props.zIndex
if (this.props.width) style2.maxWidth = ~~this.props.width
return (
<div
className={`modal rbalert ${this.props.className || ''}`}
aria-modal="true"
style={style1}
tabIndex="-1"
ref={(c) => {
this._dlg = c
this._element = c
}}>
<div className="modal-dialog modal-dialog-centered" style={style2}>
<div className="modal-content">
<div className="modal-header pb-0">
<button className="close" type="button" onClick={() => this.hide()} title={`${$L('关闭')} (Esc)`}>
<span className="zmdi zmdi-close" />
</button>
</div>
<div className="modal-body">{this.props.type === 'clear' ? this.props.message : this.renderContent()}</div>
</div>
</div>
</div>
)
}
renderContent() {
const type = this.props.type || 'primary'
let icon = this.props.icon
if (!icon) icon = type === 'danger' ? 'alert-triangle' : type === 'primary' ? 'help-outline' : 'alert-circle-o'
const _onCancel = (this.props.onCancel || this.props.cancel || this.hide).bind(this)
const _onConfirm = (this.props.onConfirm || this.props.confirm || this.hide).bind(this)
return (
<div className="text-center ml-6 mr-6">
<div className={`text-${type}`}>
<i className={`modal-main-icon zmdi zmdi-${icon}`} />
</div>
{this.props.title && <h4 className="mb-2 mt-3">{this.props.title}</h4>}
<div className={this.props.title ? '' : 'mt-3'}>
<div>{this.props.message}</div>
</div>
<div className="mt-4 mb-3">
<button disabled={this.state.disabled} className="btn btn-space btn-secondary" type="button" onClick={_onCancel} ref={(c) => (this._$cancel = c)}>
{this.props.cancelText || $L('取消')}
</button>
<button disabled={this.state.disabled} className={`btn btn-space btn-${type}`} type="button" onClick={_onConfirm} ref={(c) => (this._$btn = c)}>
{this.props.confirmText || $L('确定')}
</button>
</div>
</div>
)
}
componentDidMount() {
const $root = $(this._dlg)
.modal({ show: true, keyboard: true })
.on('hidden.bs.modal', function () {
$keepModalOpen()
$root.modal('dispose')
$unmount($root.parent())
})
// z-index
setTimeout(() => {
const mb = $('.modal-backdrop.show')
if (mb.length > 1) $(mb[mb.length - 1]).addClass('rbalert')
}, 0)
if (this.props.countdown > 0) {
$countdownButton($(this._$btn), this.props.countdown)
}
}
componentWillUnmount() {
if (this.__select2) {
__destroySelect2(this.__select2)
this.__select2 = null
}
}
hide(forceHide) {
if (forceHide) $(this._dlg).off('hide.bs.modal')
$(this._dlg).modal('hide')
}
disabled(d, preventHide) {
d = d === true
// 带有 tabIndex=-1 导致 select2 组件搜索框无法搜索???
this.setState({ disabled: d }, () => _preventHide(d, preventHide, this._dlg))
}
// -- Usage
/**
* @param {*} message
* @param {*} titleOrOption
* @param {*} option
*/
static create(message, titleOrOption, option) {
if (typeof titleOrOption === 'object') {
option = titleOrOption
titleOrOption = null
}
option = option || {}
const props = { ...option, title: titleOrOption, message: message }
renderRbcomp(<RbAlert {...props} />, null, option.onRendered || option.call)
}
}
function _preventHide(d, preventHide, dlg) {
if (d && preventHide) {
$(dlg).find('.close').attr('disabled', true)
$(dlg)
.off('hide.bs.modal')
.on('hide.bs.modal', function () {
if (event && event.target && $(event.target).hasClass('zmdi-close')) {
RbHighbar.create($L('请等待请求执行完毕'))
}
return false
})
} else if (!d) {
$(dlg).find('.close').attr('disabled', false)
$(dlg)
.off('hide.bs.modal')
.on('hide.bs.modal', function () {
return true
})
}
}
// ~~ 顶部提示条
class RbHighbar extends React.Component {
constructor(props) {
super(props)
this.state = { animatedClass: 'slideInDown' }
const n = $('.rbhighbar').length
if (n > 0 && n < 5) this._offsetTop = n * 62
}
render() {
let icon = this.props.type === 'success' ? 'check' : 'info-outline'
icon = this.props.type === 'danger' ? 'close-circle-o' : icon
return (
<div ref={(c) => (this._element = c)} className={`rbhighbar animated faster ${this.state.animatedClass}`} style={{ top: this._offsetTop || 0 }}>
<div className={`alert alert-dismissible alert-${this.props.type || 'warning'} mb-0`}>
<button className="close" type="button" onClick={this.close} title={$L('关闭')}>
<i className="zmdi zmdi-close" />
</button>
<div className="icon">
<i className={`zmdi zmdi-${icon}`} />
</div>
<div className="message pl-0">{this.props.message}</div>
</div>
</div>
)
}
componentDidMount() {
setTimeout(() => this.close(), this.props.timeout || 3000)
}
close = () => {
this.setState({ animatedClass: 'fadeOut' })
setTimeout(() => $unmount($(this._element).parent(), 20), 200)
setTimeout(() => {
$('.rbhighbar').each(function () {
const top = ~~($(this).css('top') || '0').replace('px', '')
if (top >= 62) $(this).animate({ top: top - 62 }, 100)
})
}, 100)
}
// -- Usage
/**
* @param {*} message
* @param {*} option
*/
static create(message, option) {
if (top !== self && parent.RbHighbar) {
parent.RbHighbar.create(message, option)
} else {
option = option || {}
renderRbcomp(<RbHighbar message={message} type={option.type} timeout={option.timeout} />)
}
}
/**
* @param {*} message
* @param {*} option
*/
static createl(message, option) {
return RbHighbar.create($L(message), option)
}
/**
* @param {*} message
*/
static success(message) {
RbHighbar.create(message || $L('操作成功'), { type: 'success' })
}
/**
* @param {*} message
*/
static error(message) {
RbHighbar.create(message || $L('系统繁忙请稍后重试'), { type: 'danger', timeout: 5000 })
}
}
// ~~ 提示条
class RbAlertBox extends React.Component {
render() {
const props = this.props
const type = (props || {}).type || 'warning'
let icon = props.icon
if (!icon) icon = type === 'success' ? 'check' : type === 'danger' ? 'close-circle-o' : 'info-outline'
return (
<div className={`alert alert-icon alert-icon-border alert-sm alert-${type} ${props.unclose ? '' : 'alert-dismissible'} ${props.className || ''}`} ref={(c) => (this._element = c)}>
<div className="icon">
<i className={`zmdi zmdi-${icon}`} />
</div>
<div className="message">
{props.unclose ? null : (
<a className="close" onClick={() => this._handleClose()} title={$L('关闭')} data-dismiss="alert">
<i className="zmdi zmdi-close" />
</a>
)}
<div>{props.message || 'INMESSAGE'}</div>
</div>
</div>
)
}
_handleClose() {
// $unmount($(this._element).parent(), 10, true)
typeof this.props.onClose === 'function' && this.props.onClose()
}
}
// ~~ 加载动画 @see spinner.html
function RbSpinner(props) {
const spinner = (
<div className="rb-spinner">
{$.browser.msie ? (
<span className="spinner-border spinner-border-xl text-primary" />
) : (
<svg width="40px" height="40px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle fill="none" strokeWidth="4" strokeLinecap="round" cx="33" cy="33" r="30" className="circle" />
</svg>
)}
</div>
)
if (props && props.fully === true) return <div className="rb-loading rb-loading-active">{spinner}</div>
return spinner
}
// ~~ 用户选择器
class UserSelector extends React.Component {
constructor(props) {
super(props)
this.state = { selected: props.selected || [] }
this._cached = {}
this._useTabs = []
if (props.hideUser !== true) this._useTabs.push(['User', $L('用户')])
if (props.hideDepartment !== true) this._useTabs.push(['Department', $L('部门')])
if (props.hideRole !== true && rb.isAdminUser) this._useTabs.push(['Role', $L('角色')]) // v3.2 Only admin
if (props.hideTeam !== true) this._useTabs.push(['Team', $L('团队')])
// 默认显示
this.state.tabType = this._useTabs[0] ? this._useTabs[0][0] : null
}
render() {
let inResult
if (!this.state.items) {
inResult = <li className="select2-results__option un-hover text-muted">{$L('搜索中')}</li>
} else if (this.state.items.length === 0) {
inResult = <li className="select2-results__option un-hover">{$L('未找到结果')}</li>
} else {
inResult = this.state.items.map((item) => {
return (
<li key={item.id} className="select2-results__option" data-id={item.id} onClick={(e) => this.clickItem(e)}>
<i className={`zmdi ${!this.props.hideSelection && this.containsItem(item.id) ? ' zmdi-check' : ''}`} />
{this.state.tabType === 'User' && <img src={`${rb.baseUrl}/account/user-avatar/${item.id}`} className="avatar" alt="Avatar" />}
<span className="text">{item.text}</span>
</li>
)
})
}
const dropdownMenu = (
<div className="dropdown-menu">
<div className="selector-search">
<div>
<input
type="search"
className="form-control search"
placeholder={$L('输入关键词搜索')}
value={this.state.query || ''}
ref={(c) => (this._$input = c)}
onChange={(e) => this.search(e)}
onKeyDown={(e) => this._keyEvent(e)}
/>
</div>
</div>
<div className="tab-container m-0">
<ul className={`nav nav-tabs nav-tabs-classic ${this._useTabs.length < 2 ? 'hide' : ''} ${this._useTabs.length > 4 ? 'w5' : ''}`}>
{this._useTabs.map((item) => {
return (
<li className="nav-item" key={`t-${item[0]}`}>
<a onClick={() => this.switchTab(item[0])} className={`nav-link ${this.state.tabType === item[0] ? ' active' : ''}`}>
{item[1]}
</a>
</li>
)
})}
</ul>
<div className="tab-content">
<div className="tab-pane active">
<div className="rb-scroller" ref={(c) => (this._$scroller = c)}>
<ul className="select2-results__options">{inResult}</ul>
</div>
</div>
</div>
</div>
</div>
)
// Mix select2 and dropdown
// data-reference="parent" data-offset="1,26"
return (
<span className="select2 select2-container select2-container--default user-selector">
<span className="selection">
{this.props.compToggle ? (
<span ref={(c) => (this._$dropdownParent = c)}>
{this.props.compToggle}
{dropdownMenu}
</span>
) : (
<div className="select2-selection select2-selection--multiple">
<div className="select2-selection__rendered" ref={(c) => (this._$dropdownParent = c)}>
{this.state.selected.length > 0 && (
<span className="select2-selection__clear" onClick={() => this.clearSelection()}>
&times;
</span>
)}
{this.state.selected.map((item) => {
return (
<span key={item.id} className="select2-selection__choice">
<span className="select2-selection__choice__remove" data-id={item.id} onClick={(e) => this.removeItem(e)}>
&times;
</span>
{item.text}
</span>
)
})}
<span className="select2-selection__choice abtn" data-toggle="dropdown">
<a>
<i className="zmdi zmdi-plus" /> {this.props.multiple === false ? $L('选择') : $L('添加')}
</a>
</span>
{dropdownMenu}
</div>
</div>
)}
</span>
</span>
)
}
componentDidMount() {
$(this._$scroller).perfectScrollbar()
const that = this
$(this._$dropdownParent).on({
'shown.bs.dropdown': function () {
// 初始化
if (!that.state.items) that.switchTab()
that._$input && that._$input.focus()
$(that._$scroller).find('li.active').removeClass('active')
},
'hide.bs.dropdown': function (e) {
if (!e.clickEvent || !e.clickEvent.target) return
const $target = $(e.clickEvent.target)
if ($target.hasClass('dropdown-menu') || $target.parents('.dropdown-menu').length === 1) {
if (that.props.multiple === false) {
if (!($target.hasClass('select2-results__options') || $target.parents('.select2-results__options').length === 1)) return false
} else {
return false
}
}
},
})
this.props.defaultValue && this._renderValue(this.props.defaultValue)
}
_renderValue(value) {
let s = value
if ($type(s) === 'string') s = s.split(',')
$.post('/commons/search/user-selector', JSON.stringify(s), (res) => {
if (res.error_code === 0 && res.data.length > 0) {
this.setState({ selected: res.data })
}
})
}
componentWillUnmount() {
$(this._$scroller).perfectScrollbar('destroy')
}
UNSAFE_componentWillReceiveProps(props) {
this.setState({ selected: props.selected || this.state.selected })
}
clearSelection() {
this.setState({ selected: [] }, () => {
typeof this.props.onClearSelection === 'function' && this.props.onClearSelection()
})
}
switchTab(type) {
type = type || this.state.tabType
const ckey = `${type}-${this.state.query}`
this.setState({ tabType: type, items: this._cached[ckey] }, () => {
if (!this._cached[ckey]) {
$.get(`/commons/search/users?type=${type}&q=${$encode(this.state.query)}&atall=${!!this.props.requestAtAll}`, (res) => {
this._cached[ckey] = res.data
this.switchTab(type)
})
}
$(this._$scroller).perfectScrollbar('update')
})
}
_tryActive($active, $el) {
if ($el && $el.length === 1) {
$active.removeClass('active')
$el.addClass('active')
const st = $(this._$scroller).scrollTop()
const et = $el.position().top
if (et >= 0) {
const top = et + st - (222 - 36) // maxHeight - elementHeight
if (top > 0) $(this._$scroller).scrollTop(top)
} else {
const top = st + et
if (top >= 0) $(this._$scroller).scrollTop(top)
}
}
}
_keyEvent(e) {
if (e.keyCode === 40) {
// DOWN
const $active = $(this._$scroller).find('li.active')
const $next = $active.length === 0 ? $(this._$scroller).find('li:eq(0)') : $active.next()
this._tryActive($active, $next)
} else if (e.keyCode === 38) {
// UP
const $active = $(this._$scroller).find('li.active')
const $prev = $active.length === 0 ? null : $active.prev()
this._tryActive($active, $prev)
} else if (e.keyCode === 13) {
// ENTER
e.preventDefault()
const $active = $(this._$scroller).find('li.active')
if ($active.length === 1) {
$active.trigger('click')
$stopEvent(e)
}
} else if (e.keyCode === 27) {
// ESC
e.preventDefault()
this.toggle() // hide
// Auto focus for textarea
this.props.targetInput && this.props.targetInput.focus()
}
}
search(e) {
this.setState({ query: e.target.value }, () => {
$setTimeout(() => this.switchTab(), 300, 'us-search-items')
})
}
clickItem(e, isRemove) {
const $target = $(e.currentTarget)
const id = $target.data('id') || $target.parents('.select2-results__option').data('id')
let exists = false
let ns = []
// 单选
if (this.props.multiple !== false || isRemove) {
ns = this.state.selected.filter((x) => {
if (x.id === id) {
exists = true
return false
}
return true
})
}
const selected = { id: id, text: $target.text() }
if (!exists) ns.push(selected)
if (ns.length >= 20) {
RbHighbar.create($L('最多选择 20 '))
return false
}
this.setState({ selected: ns }, () => {
typeof this.props.onSelectItem === 'function' && this.props.onSelectItem(selected, isRemove)
})
}
removeItem(e) {
this.clickItem(e, true)
}
containsItem(id) {
return !!this.state.selected.find((x) => x.id === id)
}
getSelected() {
const ids = []
this.state.selected.forEach((item) => ids.push(item.id))
return ids
}
val() {
// v3.7 set
if (arguments[0]) {
this._renderValue(arguments[0])
return
}
return this.getSelected()
}
toggle() {
// $(this._$dropdownParent).dropdown('toggle')
$(this._$dropdownParent).find('[data-toggle="dropdown"]').dropdown('toggle')
}
}
// ~~ 用户显示
const UserShow = function (props) {
const viewUrl = props.id && props.noLink !== true ? `#!/View/User/${props.id}` : null
const avatarUrl = `${rb.baseUrl}/account/user-avatar/${props.id}`
return (
<a href={viewUrl} className="user-show" title={props.name} onClick={props.onClick}>
<div className={`avatar ${props.showName === true ? ' float-left' : ''}`}>{props.icon ? <i className={props.icon} /> : <img src={avatarUrl} alt="Avatar" />}</div>
{props.showName && (
<div className={`text-truncate name ${props.deptName ? 'vm' : ''}`}>
{props.name}
{props.deptName && <em>{props.deptName}</em>}
</div>
)}
</a>
)
}
// ~~ 日期显示
class DateShow extends React.Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
const date = this.props.date
return date ? (
<span title={this.props.title || date} onClick={() => this.setState({ _showOrigin: !this.state._showOrigin })}>
{this.props.showOrigin && this.state._showOrigin ? date : $fromNow(date)}
</span>
) : null
}
}
// ~~ 记录选择器
// @see rb-page.js#$initReferenceSelect2
class RecordSelector extends React.Component {
constructor(props) {
super(props)
this.state = { ...props }
}
render() {
return (
<div className="input-group has-append">
<select className="form-control form-control-sm" ref={(c) => (this._$select = c)} />
<div className="input-group-append">
<button className="btn btn-secondary" onClick={() => this._showSearcher()} ref={(c) => (this._$btn = c)}>
<i className="icon zmdi zmdi-search" />
</button>
</div>
</div>
)
}
componentDidMount() {
this._initSelect2()
}
_initSelect2(reset) {
const props = this.state // use state
if (!props.entity) return
if (reset) {
this.reset()
this.__select2 && this.__select2.select2('destroy')
this._ReferenceSearcher && this._ReferenceSearcher.destroy()
this._ReferenceSearcher = null
}
this.__select2 = $initReferenceSelect2(this._$select, {
searchType: 'search',
entity: props.entity,
placeholder: props.entityLabel || null,
}).on('change', (e) => {
typeof props.onSelect === 'function' && props.onSelect(e.target.value)
})
}
_showSearcher() {
const that = this
window.referenceSearch__call = function (selected) {
const id = selected[0]
if ($(that._$select).find(`option[value="${id}"]`).length > 0) {
that.__select2.val(id).trigger('change')
} else {
that._setValue(id)
}
that._ReferenceSearcher.hide()
}
if (this._ReferenceSearcher) {
this._ReferenceSearcher.show()
} else {
const props = this.state // use state
const searchUrl = `${rb.baseUrl}/app/entity/reference-search?field=${props.entity}Id.${props.entity}`
renderRbcomp(<ReferenceSearcher url={searchUrl} title={$L('选择%s', props.entityLabel || '')} useWhite />, function () {
that._ReferenceSearcher = this
})
}
}
_setValue(id) {
$.get(`/commons/search/read-labels?ids=${id}`, (res) => {
const _data = res.data || {}
const o = new Option(_data[id], id, true, true)
this.__select2.append(o).trigger('change')
})
}
// return `id`
val() {
return $(this._$select).val()
}
// return `{ id:xx, text:xx }`
getValue() {
const id = this.val()
if (id) {
return {
id: id,
text: this.__select2.select2('data')[0].text,
}
}
return null
}
setValue(id, text) {
if (text) {
const o = new Option(text, id, true, true)
this.__select2.append(o).trigger('change')
} else {
this._setValue(id)
}
}
reset() {
$(this._$select).val(null).trigger('change')
}
componentWillUnmount() {
this.__select2 && this.__select2.select2('destroy')
}
}
// ~~ 任意记录选择器
class AnyRecordSelector extends RecordSelector {
render() {
const ae = this.props.allowEntities || []
return (
<div className="row">
<div className="col-4 pr-0">
<select className="form-control form-control-sm" ref={(c) => (this._$entity = c)}>
{(this.state.entities || []).map((item) => {
if ($isSysMask(item.label)) return null
return (
<option key={item.name} value={item.name}>
{item.label}
</option>
)
})}
</select>
</div>
<div className="col-8 pl-2">{super.render()}</div>
</div>
)
}
componentDidMount() {
super.componentDidMount()
$.get('/commons/metadata/entities?detail=true', (res) => {
let entities = res.data || []
if (this.props.allowEntities && this.props.allowEntities.length > 0) {
entities = entities.filter((item) => this.props.allowEntities.includes(item.name))
}
this.setState({ entities }, () => {
this.__select2Entity = $(this._$entity)
.select2({
placeholder: $L('无可用'),
allowClear: false,
templateResult: function (res) {
const $span = $('<span class="icon-append"></span>').attr('title', res.text).text(res.text)
const icon = entities.find((x) => x.entity === res.id)
$(`<i class="icon zmdi zmdi-${icon ? icon.icon : 'texture'}"></i>`).appendTo($span)
return $span
},
})
.on('change', (e) => {
if (e.target.value) {
this.setState(
{
entity: e.target.value,
entityLabel: this.__select2Entity.select2('data')[0].text,
},
() => this._initSelect2(true)
)
}
})
// init
entities[0] && $(this._$entity).val(entities[0].name).trigger('change')
// 编辑时
const iv = this.props.initValue
if (iv) {
$(this._$entity).val(iv.entity).trigger('change')
setTimeout(() => {
const o = new Option(iv.text, iv.id, true, true)
$(this._$select).append(o)
}, 200)
}
})
})
}
getValue() {
let v = super.getValue()
if (v) {
v = { ...v, entity: $(this._$entity).val() }
}
return v
}
componentWillUnmount() {
super.componentWillUnmount()
this.__select2Entity && this.__select2Entity.select2('destroy')
}
}
// ~~ 选择记录
class RecordSelectorModal extends RbAlert {
renderContent() {
return (
<div className="form ml-3 mr-3">
<div className="form-group">
<label className="text-bold">{this.props.title || $L('选择记录')}</label>
<AnyRecordSelector ref={(c) => (this._AnyRecordSelector = c)} allowEntities={this.props.allowEntities} />
</div>
<div className="form-group mb-2">
<button
type="button"
className="btn btn-primary"
onClick={() => {
typeof this.props.onConfirm === 'function' && this.props.onConfirm(this._AnyRecordSelector.val())
this.hide()
}}>
{$L('确定')}
</button>
</div>
</div>
)
}
// -- Usage
/**
* @param {*} props
*/
static create(props) {
renderRbcomp(<RecordSelectorModal {...props} zIndex="1050" />)
}
}
// ~~ 默认 EasyMDE 工具栏
const DEFAULT_MDE_TOOLBAR = (c) => {
return [
{
name: 'bold',
action: EasyMDE.toggleBold,
className: 'zmdi zmdi-format-bold',
title: $L('粗体'),
},
{
name: 'italic',
action: EasyMDE.toggleItalic,
className: 'zmdi zmdi-format-italic',
title: $L('斜体'),
},
{
name: 'strikethrough',
action: EasyMDE.toggleStrikethrough,
className: 'zmdi zmdi-format-strikethrough',
title: $L('删除线'),
},
{
name: 'heading',
action: EasyMDE.toggleHeadingSmaller,
className: 'zmdi zmdi-format-size',
title: $L('标题'),
},
{
name: 'unordered-list',
action: EasyMDE.toggleUnorderedList,
className: 'zmdi zmdi-format-list-bulleted',
title: $L('列表'),
},
{
name: 'ordered-list',
action: EasyMDE.toggleOrderedList,
className: 'zmdi zmdi-format-list-numbered',
title: $L('数字列表'),
},
{
name: 'link',
action: EasyMDE.drawLink,
className: 'zmdi zmdi-link',
title: $L('链接'),
},
{
name: 'image',
action: () => c && c._fieldValue__upload && c._fieldValue__upload.click(),
className: 'zmdi zmdi-image-o',
title: $L('图片'),
},
{
name: 'table',
action: EasyMDE.drawTable,
className: 'zmdi zmdi-border-all',
title: $L('表格'),
},
'|',
{
name: 'preview',
action: EasyMDE.togglePreview,
className: 'zmdi zmdi-eye no-disable',
title: $L('预览'),
},
{
name: 'fullscreen',
action: EasyMDE.toggleFullScreen,
className: 'zmdi zmdi-fullscreen no-disable',
title: $L('全屏'),
},
{
name: 'guide',
action: () => window.open('https://getrebuild.com/docs/markdown-guide'),
className: 'zmdi zmdi-help-outline no-disable',
title: $L('编辑器帮助'),
},
]
}
// ~~ HTML 内容
const WrapHtml = (htmlContent) => <span dangerouslySetInnerHTML={{ __html: htmlContent }} />
// ~~ MD > HTML
class Md2Html extends React.Component {
constructor(props) {
super(props)
this.state = {}
}
render() {
return <span ref={(c) => (this._$md2html = c)} dangerouslySetInnerHTML={{ __html: this.state.md2html }} />
}
componentDidMount() {
let md = this.props.markdown
if (this.props.keepHtml !== true) {
md = md.replace(/>/g, '&gt;').replace(/</g, '&lt;')
md = md.replace(/&gt; /g, '> ')
}
// 替换换行并保持表格换行
let cHtml = marked.parse(md.replace(/(?<!\|)\n(?!\|)/g, '\n'))
cHtml = cHtml.replace(/<img src="([^"]+)"/g, function (s, src) {
let srcNew = src + (src.includes('?') ? '&' : '?') + 'imageView2/2/w/1000/interlace/1/q/100'
return s.replace(src, srcNew)
})
this.setState({ md2html: cHtml }, () => {
$(this._$md2html)
.find('a')
.each(function () {
const $a = $(this)
$a.attr({
href: `${rb.baseUrl}/commons/url-safe?url=${encodeURIComponent($a.attr('href'))}`,
target: '_blank',
}).on('click', (e) => {
$stopEvent(e, false)
})
})
// 图片预览
let imgs = []
$(this._$md2html)
.find('img[src]')
.each(function () {
const $img = $(this)
let isrc = $img.attr('src')
if (isrc) {
if (isrc.includes('/filex/img/')) {
isrc = isrc.split('/filex/img/')[1].split(/[?&]imageView2/)[0]
}
imgs.push(isrc)
$img.on('click', (e) => {
$stopEvent(e, true)
const p = parent || window
p.RbPreview.create(imgs, imgs.indexOf(isrc) || 0)
})
}
})
})
}
}
// ~~ short React.Fragment
const RF = ({ children }) => <React.Fragment>{children}</React.Fragment>
class RbGritter extends React.Component {
constructor(props) {
super(props)
this.state = { ...props }
}
render() {
return (
<div id="gritter-notice-wrapper" className="gritter-main-wrapper">
{this.state.items}
</div>
)
}
_addItem(message, option) {
const itemid = `gritter-item-${option.id || $random()}`
const type = option.type || 'success'
const icon = option.icon || (type === 'success' ? 'check' : type === 'danger' ? 'close-circle-o' : 'info-outline')
const item = (
<div key={itemid} id={itemid} className={`gritter-item-wrapper color ${type} animated faster fadeInRight`}>
<div className="gritter-item">
<div className="gritter-img-container">
<span className="gritter-image">
<i className={`icon zmdi zmdi-${icon}`} />
</span>
</div>
<div className="gritter-content gritter-with-image">
<a
className="gritter-close"
onClick={(e) => {
$stopEvent(e, true)
this._removeItem(itemid)
typeof option.onCancel === 'function' && option.onCancel()
}}
title={$L('关闭')}
/>
{option.title && <span className="gritter-title">{option.title}</span>}
<p>{message}</p>
</div>
</div>
</div>
)
const itemsNew = this.state.items || []
itemsNew.push(item)
this.setState({ items: itemsNew }, () => {
setTimeout(() => this._removeItem(itemid), option.timeout || 6000)
})
}
_removeItem(itemid) {
const itemsNew = this.state.items.filter((item) => itemid !== item.key)
this.setState({ items: itemsNew })
}
// -- Usage
/**
* @param {*} message
* @param {*} option
*/
static create(message, option = {}) {
if (top !== self && parent.RbGritter) {
parent.RbGritter.create(message, option)
} else {
if (this._RbGritter) {
this._RbGritter._addItem(message, option)
} else {
const that = this
renderRbcomp(<RbGritter />, function () {
that._RbGritter = this
that._RbGritter._addItem(message, option)
})
}
}
}
/**
* @param {*} id
*/
static remove(id) {
if (this._RbGritter) {
if (id) id = 'gritter-item-' + id
this._RbGritter._removeItem(id)
}
}
}
// ~~ 代码查看
class CodeViewport extends React.Component {
render() {
return (
<div className="code-viewport">
<pre ref={(c) => (this._$code = c)}>LOADING</pre>
{window.ClipboardJS && (
<a className="copy" title={$L('复制')} ref={(c) => (this._$copy = c)}>
<i className="icon zmdi zmdi-copy" />
</a>
)}
</div>
)
}
componentDidMount() {
this._$code.innerHTML = $formattedCode(this.props.code || '', this.props.type)
if (this._$copy) {
const that = this
const $copy = $(this._$copy).on('mouseenter', () => $(this._$copy).removeClass('copied-check'))
// eslint-disable-next-line no-undef
new ClipboardJS($copy[0], {
text: function () {
return $(that._$code).text()
},
}).on('success', () => $copy.addClass('copied-check'))
}
}
UNSAFE_componentWillReceiveProps(newProps) {
// eslint-disable-next-line eqeqeq
if (newProps.code && newProps.code != this.props.code) {
this._$code.innerHTML = $formattedCode(newProps.code, this.props.type)
}
}
}
// ~~ 树组件 v2.5 v3.7
// TODO 子级延迟渲染
class AsideTree extends React.Component {
constructor(props) {
super(props)
this.state = { ...props, expandItems: [] }
this.__items = {}
}
render() {
return <div className={`aside-2tree ${this.props.hideCollapse ? 'hide-collapse' : ''}`}>{this.renderTree(this.props.data || [])}</div>
}
renderTree(items, item) {
return (
<ul className={`list-unstyled m-0 ${item && !this.state.expandItems.contains(item.id) ? 'hide' : ''}`}>
{items.map((item) => {
let $children = null
if (item.children && item.children.length > 0) {
$children = this.renderTree(item.children, item)
}
const $item = this.renderItem(item, $children !== null)
return (
<RF key={item.id}>
{$item}
{$children}
</RF>
)
})}
</ul>
)
}
renderItem(item, hasChild) {
this.__items[item.id] = item
return (
<li className={this.state.activeItem === item.id ? 'active' : ''}>
<span
className={`collapse-icon ${hasChild ? '' : 'no-child'}`}
onClick={() => {
if (hasChild) {
const expandItemsNew = this.state.expandItems
expandItemsNew.toggle(item.id)
this.setState({ expandItems: expandItemsNew })
}
}}>
<i className={`zmdi zmdi-chevron-right ${this.state.expandItems.contains(item.id) ? 'open' : ''} `} />
</span>
<a
data-id={item.id}
className={`text-ellipsis ${item.disabled ? 'text-disabled' : ''}`}
title={item.disabled ? $L('已禁用') : null}
onClick={() => {
this.setState({ activeItem: item.id }, () => {
typeof this.props.onItemClick === 'function' && this.props.onItemClick(item)
})
}}>
{this.props.icon && <i className={`icon ${this.props.icon}`} />}
{item.text || item.name}
{item.private === true && <i className="icon flag zmdi zmdi-lock" title={$L('私有')} />}
{!!item.specUsers && <i className="icon flag zmdi zmdi-account" title={$L('指定用户')} />}
</a>
{typeof this.props.extrasAction === 'function' && this.props.extrasAction(item)}
</li>
)
}
refresh(data) {
this.__items = {}
this.setState({ data: data })
}
triggerClick(id) {
const item = this.__items[id]
if (item) {
this.setState({ activeItem: item.id }, () => {
typeof this.props.onItemClick === 'function' && this.props.onItemClick(item)
// 树状需要展开父级
let ps = []
let loop = item
while (loop.parent) {
ps.push(loop.parent)
loop = this.__items[loop.parent]
}
ps.length > 0 && this.setState({ expandItems: ps })
})
}
}
// 获取所有子级 ID
static findAllChildIds(item) {
function _find(x, into) {
into.push(x.id)
if (x.children && x.children.length > 0) {
x.children.forEach((xx) => _find(xx, into))
}
}
const s = []
_find(item, s)
return s
}
}
/**
* JSX 组件渲染
*
* @param {*} JSX
* @param {*} container id or object of element (or function of callback)
* @param {*} callback callback on mounted
* @param {*} v18
*/
const renderRbcomp = function (JSX, container, callback, v18) {
if (typeof container === 'function') {
callback = container
container = null
}
container = container || $random('react-container-', true, 32)
if (typeof container === 'string') {
const c = document.getElementById(container)
if (c) {
container = c
} else {
if (container.startsWith('react-container-')) container = $(`<div id="${container}"></div>`).appendTo(document.body)[0]
else throw 'No element found : ' + container
}
} else if (container instanceof $) {
container = container[0]
}
if (v18 && !!ReactDOM.createRoot) {
const root = ReactDOM.createRoot(container)
const JSX18 = React.cloneElement(JSX, { __root18: root })
root.render(JSX18)
return root
}
ReactDOM.render(JSX, container, callback)
return container
}
// 渲染可重用组件
const __DLGCOMPS = {}
const renderDlgcomp = function (JSX, id) {
if (__DLGCOMPS[id]) {
__DLGCOMPS[id].show()
} else {
renderRbcomp(JSX, function () {
__DLGCOMPS[id] = this
})
}
}
// for: React v18
const renderRbcomp18 = function (JSX, container) {
return renderRbcomp(JSX, container, null, true)
}
// destroy select2
function __destroySelect2(__select2) {
if (__select2) {
if (Array.isArray(__select2)) {
__select2.forEach((s) => s.select2('destroy'))
} else {
__select2.select2('destroy')
}
}
}