提交 75d36c63 编写于 作者: Q qiang

Merge branch 'editor' into v3

...@@ -8,7 +8,7 @@ editor 组件对应的 editorContext 实例,可通过 [uni.createSelectorQuery ...@@ -8,7 +8,7 @@ editor 组件对应的 editorContext 实例,可通过 [uni.createSelectorQuery
|App|H5|微信小程序|支付宝小程序|百度小程序|头条小程序|QQ小程序| |App|H5|微信小程序|支付宝小程序|百度小程序|头条小程序|QQ小程序|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|√|x|√|x|x|x|x| |√|2.4.5+|√|x|x|x|x|
## editorContext.format(name, value) ## editorContext.format(name, value)
......
## uni.createSelectorQuery() ## uni.createSelectorQuery()
返回一个 ``SelectorQuery`` 对象实例。可以在这个实例上使用 ``select`` 等方法选择节点,并使用 ``boundingClientRect`` 等方法选择需要查询的信息。 返回一个 ``SelectorQuery`` 对象实例。可以在这个实例上使用 ``select`` 等方法选择节点,并使用 ``boundingClientRect`` 等方法选择需要查询的信息。
**Tips:** **Tips:**
...@@ -16,12 +16,12 @@ ...@@ -16,12 +16,12 @@
将选择器的选取范围更改为自定义组件 ``component`` 内,返回一个 ``SelectorQuery`` 对象实例。(初始时,选择器仅选取页面范围的节点,不会选取任何自定义组件中的节点)。 将选择器的选取范围更改为自定义组件 ``component`` 内,返回一个 ``SelectorQuery`` 对象实例。(初始时,选择器仅选取页面范围的节点,不会选取任何自定义组件中的节点)。
**代码示例** **代码示例**
```javascript ```javascript
const query = uni.createSelectorQuery().in(this); const query = uni.createSelectorQuery().in(this);
query.select('#id').boundingClientRect(data => { query.select('#id').boundingClientRect(data => {
console.log("得到布局位置信息" + JSON.stringify(data)); console.log("得到布局位置信息" + JSON.stringify(data));
console.log("节点离页面顶部的距离为" + data.top); console.log("节点离页面顶部的距离为" + data.top);
}).exec(); }).exec();
``` ```
### selectorQuery.select(selector) ### selectorQuery.select(selector)
...@@ -36,21 +36,21 @@ query.select('#id').boundingClientRect(data => { ...@@ -36,21 +36,21 @@ query.select('#id').boundingClientRect(data => {
- 子元素选择器:``.the-parent > .the-child`` - 子元素选择器:``.the-parent > .the-child``
- 后代选择器:``.the-ancestor .the-descendant`` - 后代选择器:``.the-ancestor .the-descendant``
- 跨自定义组件的后代选择器:``.the-ancestor >>> .the-descendant`` - 跨自定义组件的后代选择器:``.the-ancestor >>> .the-descendant``
- 多选择器的并集:``#a-node, .some-other-nodes`` - 多选择器的并集:``#a-node, .some-other-nodes``
### selectorQuery.selectAll(selector) ### selectorQuery.selectAll(selector)
在当前页面下选择匹配选择器 ``selector`` 的所有节点,返回一个 ``NodesRef`` 对象实例,可以用于获取节点信息。 在当前页面下选择匹配选择器 ``selector`` 的所有节点,返回一个 ``NodesRef`` 对象实例,可以用于获取节点信息。
### selectorQuery.selectViewport() ### selectorQuery.selectViewport()
选择显示区域,可用于获取显示区域的尺寸、滚动位置等信息,返回一个 ``NodesRef`` 对象实例。 选择显示区域,可用于获取显示区域的尺寸、滚动位置等信息,返回一个 ``NodesRef`` 对象实例。
### selectorQuery.exec(callback) ### selectorQuery.exec(callback)
执行所有的请求。请求结果按请求次序构成数组,在callback的第一个参数中返回。 执行所有的请求。请求结果按请求次序构成数组,在callback的第一个参数中返回。
## NodesRef ## NodesRef
用于获取节点信息的对象 用于获取节点信息的对象
...@@ -110,37 +110,37 @@ query.select('#id').boundingClientRect(data => { ...@@ -110,37 +110,37 @@ query.select('#id').boundingClientRect(data => {
|App|H5|微信小程序|支付宝小程序|百度小程序|头条小程序|QQ小程序| |App|H5|微信小程序|支付宝小程序|百度小程序|头条小程序|QQ小程序|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|√|x|√|x|x|x|√| |√|2.4.5+|√|x|x|x|√|
**callback 返回参数** **callback 返回参数**
| 属性 | 类型 | 说明 | | 属性 | 类型 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| context | Object | 节点对应的 Context 对象 | | context | Object | 节点对应的 Context 对象 |
### 代码示例 ### 代码示例
```javascript ```javascript
uni.createSelectorQuery().selectViewport().scrollOffset(res => { uni.createSelectorQuery().selectViewport().scrollOffset(res => {
console.log("竖直滚动位置" + res.scrollTop); console.log("竖直滚动位置" + res.scrollTop);
}).exec(); }).exec();
let view = uni.createSelectorQuery().in(this).select(".test"); let view = uni.createSelectorQuery().in(this).select(".test");
view.fields({ view.fields({
size: true, size: true,
scrollOffset: true scrollOffset: true
}, data => { }, data => {
console.log("得到节点信息" + JSON.stringify(data)); console.log("得到节点信息" + JSON.stringify(data));
console.log("节点的宽为" + data.width); console.log("节点的宽为" + data.width);
}).exec(); }).exec();
view.boundingClientRect(data => { view.boundingClientRect(data => {
console.log("得到布局位置信息" + JSON.stringify(data)); console.log("得到布局位置信息" + JSON.stringify(data));
console.log("节点离页面顶部的距离为" + data.top); console.log("节点离页面顶部的距离为" + data.top);
}).exec(); }).exec();
``` ```
**注意** **注意**
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
通过`setContents`接口设置内容时,解析插入的 `html` 可能会由于一些非法标签导致解析错误,建议开发者在应用内使用时通过 delta 进行插入。 通过`setContents`接口设置内容时,解析插入的 `html` 可能会由于一些非法标签导致解析错误,建议开发者在应用内使用时通过 delta 进行插入。
富文本组件内部引入了一些基本的样式使得内容可以正确的展示,开发时可以进行覆盖。需要注意的是,在其它组件或环境中使用富文本组件导出的html时,需要额外引入[这段样式](https://github.com/wechat-miniprogram/editor-style/blob/master/editor.css),并维护`<ql-container><ql-editor></ql-editor></ql-container>`的结构,参考:[使用 editor 组件导出的 html](https://ask.dcloud.net.cn/article/36205) 富文本组件内部引入了一些基本的样式使得内容可以正确的展示,开发时可以进行覆盖。需要注意的是,在其它组件或环境中使用富文本组件导出的html时,需要额外引入[这段样式](https://github.com/dcloudio/uni-app/blob/master/src/core/view/components/editor/editor.css),并维护`<ql-container><ql-editor></ql-editor></ql-container>`的结构,参考:[使用 editor 组件导出的 html](https://ask.dcloud.net.cn/article/36205)
图片控件仅初始化时设置有效。 图片控件仅初始化时设置有效。
...@@ -16,11 +16,11 @@ ...@@ -16,11 +16,11 @@
|App|H5|微信小程序|支付宝小程序|百度小程序|头条小程序|QQ小程序| |App|H5|微信小程序|支付宝小程序|百度小程序|头条小程序|QQ小程序|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:| |:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|2.0.0+ [自定义组件编译模式](https://ask.dcloud.net.cn/article/35843),不含nvue|x|基础库 2.7.0+|x|x|x|x| |2.0.0+ [自定义组件编译模式](https://ask.dcloud.net.cn/article/35843),不含nvue|2.4.5+|基础库 2.7.0+|x|x|x|x|
本功能自HBuilderX2.0起支持。运行到微信小程序工具时,注意在微信工具里选择最新的基础库。 本功能自HBuilderX2.0起支持。运行到微信小程序工具时,注意在微信工具里选择最新的基础库。
editor组件目前只有App的vue页面和微信支持,其他端的富文本编辑解决方案,可使用web-view加载web页面,也可搜索[插件市场](https://ext.dcloud.net.cn/search?q=%E5%AF%8C%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91) 获取简单的markdown富文本编辑器 editor组件目前只有H5、App的vue页面和微信支持,其他端的富文本编辑解决方案,可使用web-view加载web页面,也可搜索[插件市场](https://ext.dcloud.net.cn/search?q=%E5%AF%8C%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91) 获取简单的markdown富文本编辑器
| 属性 | 类型 | 默认值 | 必填 | 说明 | | 属性 | 类型 | 默认值 | 必填 | 说明 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
......
...@@ -111,9 +111,9 @@ const device = [ ...@@ -111,9 +111,9 @@ const device = [
'onBeaconUpdate', 'onBeaconUpdate',
'getBeacons', 'getBeacons',
'startBeaconDiscovery', 'startBeaconDiscovery',
'stopBeaconDiscovery', 'stopBeaconDiscovery',
'checkIsSupportSoterAuthentication', 'checkIsSupportSoterAuthentication',
'checkIsSoterEnrolledInDevice', 'checkIsSoterEnrolledInDevice',
'startSoterAuthentication' 'startSoterAuthentication'
] ]
...@@ -194,9 +194,9 @@ const third = [ ...@@ -194,9 +194,9 @@ const third = [
'onPush', 'onPush',
'offPush', 'offPush',
'requireNativePlugin', 'requireNativePlugin',
'upx2px', 'upx2px',
'restoreGlobal', 'restoreGlobal',
'getSubNVueById', 'getSubNVueById',
'getCurrentSubNVue' 'getCurrentSubNVue'
] ]
......
...@@ -152,7 +152,7 @@ ...@@ -152,7 +152,7 @@
"uni.stopPullDownRefresh": true, "uni.stopPullDownRefresh": true,
"uni.createSelectorQuery": true, "uni.createSelectorQuery": true,
"uni.createIntersectionObserver": true, "uni.createIntersectionObserver": true,
"uni.hideKeyboard": true, "uni.hideKeyboard": true,
"uni.onKeyboardHeightChange": true "uni.onKeyboardHeightChange": true
} }
}, { }, {
...@@ -206,4 +206,4 @@ ...@@ -206,4 +206,4 @@
"uni.base64ToArrayBuffer": true, "uni.base64ToArrayBuffer": true,
"uni.arrayBufferToBase64": true "uni.arrayBufferToBase64": true
} }
}] }]
module.exports = { module.exports = {
'resize-sensor': ['h5'], 'resize-sensor': ['h5'],
'ad': ['mp-weixin'], 'ad': ['mp-weixin'],
'audio': ['app-plus', 'mp-weixin', 'h5'], 'audio': ['app-plus', 'mp-weixin', 'h5'],
...@@ -9,6 +9,7 @@ module.exports = { ...@@ -9,6 +9,7 @@ module.exports = {
'checkbox-group': ['app-plus', 'mp-weixin', 'h5'], 'checkbox-group': ['app-plus', 'mp-weixin', 'h5'],
'cover-image': ['app-plus', 'mp-weixin'], 'cover-image': ['app-plus', 'mp-weixin'],
'cover-view': ['app-plus', 'mp-weixin'], 'cover-view': ['app-plus', 'mp-weixin'],
'editor': ['app-plus', 'mp-weixin', 'h5'],
'form': ['app-plus', 'mp-weixin', 'h5'], 'form': ['app-plus', 'mp-weixin', 'h5'],
'functional-page-navigator': ['mp-weixin'], 'functional-page-navigator': ['mp-weixin'],
'icon': ['app-plus', 'mp-weixin'], 'icon': ['app-plus', 'mp-weixin'],
...@@ -40,4 +41,4 @@ module.exports = { ...@@ -40,4 +41,4 @@ module.exports = {
'video': ['app-plus', 'mp-weixin', 'h5'], 'video': ['app-plus', 'mp-weixin', 'h5'],
'view': ['app-plus', 'mp-weixin', 'h5'], 'view': ['app-plus', 'mp-weixin', 'h5'],
'web-view': ['app-plus', 'mp-weixin'] 'web-view': ['app-plus', 'mp-weixin']
} }
因为 它太大了无法显示 source diff 。你可以改为 查看blob
...@@ -20,6 +20,7 @@ module.exports = [ ...@@ -20,6 +20,7 @@ module.exports = [
'uni-checkbox-group', 'uni-checkbox-group',
'uni-cover-image', 'uni-cover-image',
'uni-cover-view', 'uni-cover-view',
'uni-editor',
'uni-form', 'uni-form',
'uni-functional-page-navigator', 'uni-functional-page-navigator',
'uni-icon', 'uni-icon',
......
...@@ -258,7 +258,7 @@ var methods3 = ['setFillStyle', 'setTextAlign', 'setStrokeStyle', 'setGlobalAlph ...@@ -258,7 +258,7 @@ var methods3 = ['setFillStyle', 'setTextAlign', 'setStrokeStyle', 'setGlobalAlph
'setTextBaseline', 'setLineDash' 'setTextBaseline', 'setLineDash'
] ]
class CanvasContext { export class CanvasContext {
constructor (id, pageId) { constructor (id, pageId) {
this.id = id this.id = id
this.pageId = pageId this.pageId = pageId
......
...@@ -7,7 +7,7 @@ function operateMapPlayer (mapId, pageVm, type, data) { ...@@ -7,7 +7,7 @@ function operateMapPlayer (mapId, pageVm, type, data) {
invokeMethod('operateMapPlayer', mapId, pageVm, type, data) invokeMethod('operateMapPlayer', mapId, pageVm, type, data)
} }
class MapContext { export class MapContext {
constructor (id, pageVm) { constructor (id, pageVm) {
this.id = id this.id = id
this.pageVm = pageVm this.pageVm = pageVm
...@@ -43,4 +43,4 @@ export function createMapContext (id, context) { ...@@ -43,4 +43,4 @@ export function createMapContext (id, context) {
return new MapContext(id, context) return new MapContext(id, context)
} }
return new MapContext(id, getCurrentPageVm('createMapContext')) return new MapContext(id, getCurrentPageVm('createMapContext'))
} }
...@@ -9,7 +9,7 @@ function operateVideoPlayer (videoId, pageVm, type, data) { ...@@ -9,7 +9,7 @@ function operateVideoPlayer (videoId, pageVm, type, data) {
invokeMethod('operateVideoPlayer', videoId, pageVm, type, data) invokeMethod('operateVideoPlayer', videoId, pageVm, type, data)
} }
class VideoContext { export class VideoContext {
constructor (id, pageVm) { constructor (id, pageVm) {
this.id = id this.id = id
this.pageVm = pageVm this.pageVm = pageVm
...@@ -25,8 +25,8 @@ class VideoContext { ...@@ -25,8 +25,8 @@ class VideoContext {
operateVideoPlayer(this.id, this.pageVm, 'stop') operateVideoPlayer(this.id, this.pageVm, 'stop')
} }
seek (position) { seek (position) {
operateVideoPlayer(this.id, this.pageVm, 'seek', { operateVideoPlayer(this.id, this.pageVm, 'seek', {
position position
}) })
} }
sendDanmu (args) { sendDanmu (args) {
...@@ -36,8 +36,8 @@ class VideoContext { ...@@ -36,8 +36,8 @@ class VideoContext {
if (!~RATES.indexOf(rate)) { if (!~RATES.indexOf(rate)) {
rate = 1.0 rate = 1.0
} }
operateVideoPlayer(this.id, this.pageVm, 'playbackRate', { operateVideoPlayer(this.id, this.pageVm, 'playbackRate', {
rate rate
}) })
} }
requestFullScreen (args = {}) { requestFullScreen (args = {}) {
...@@ -59,4 +59,4 @@ export function createVideoContext (id, context) { ...@@ -59,4 +59,4 @@ export function createVideoContext (id, context) {
return new VideoContext(id, context) return new VideoContext(id, context)
} }
return new VideoContext(id, getCurrentPageVm('createVideoContext')) return new VideoContext(id, getCurrentPageVm('createVideoContext'))
} }
import {
callback
} from 'uni-shared'
function operateEditor (componentId, pageId, type, data) {
UniServiceJSBridge.publishHandler(pageId + '-editor-' + componentId, {
componentId,
type,
data
}, pageId)
}
UniServiceJSBridge.subscribe('onEditorMethodCallback', ({
callbackId,
data
}) => {
callback.invoke(callbackId, data)
})
const methods = ['insertDivider', 'insertImage', 'insertText', 'setContents', 'getContents', 'clear', 'removeFormat', 'undo', 'redo']
export class EditorContext {
constructor (id, pageId) {
this.id = id
this.pageId = pageId
}
format (name, value) {
operateEditor(this.id, this.pageId, 'format', {
options: {
name,
value
}
})
}
}
methods.forEach(function (method) {
EditorContext.prototype[method] = callback.warp(function (options, callbackId) {
operateEditor(this.id, this.pageId, method, {
options,
callbackId
})
})
})
...@@ -7,6 +7,26 @@ import { ...@@ -7,6 +7,26 @@ import {
getCurrentPageVm getCurrentPageVm
} from '../../platform' } from '../../platform'
import { CanvasContext } from '../context/canvas'
import { MapContext } from '../context/create-map-context'
import { VideoContext } from '../context/create-video-context'
import { EditorContext } from '../context/editor'
const ContextClasss = {
canvas: CanvasContext,
map: MapContext,
video: VideoContext,
editor: EditorContext
}
function convertContext (result) {
if (result.context) {
const { id, name, page } = result.context
const ContextClass = ContextClasss[name]
result.context = ContextClass && new ContextClass(id, page)
}
}
class NodesRef { class NodesRef {
constructor (selectorQuery, component, selector, single) { constructor (selectorQuery, component, selector, single) {
this._selectorQuery = selectorQuery this._selectorQuery = selectorQuery
...@@ -53,6 +73,18 @@ class NodesRef { ...@@ -53,6 +73,18 @@ class NodesRef {
) )
return this._selectorQuery return this._selectorQuery
} }
context (callback) {
this._selectorQuery._push(
this._selector,
this._component,
this._single, {
context: true
},
callback
)
return this._selectorQuery
}
} }
class SelectorQuery { class SelectorQuery {
...@@ -66,6 +98,11 @@ class SelectorQuery { ...@@ -66,6 +98,11 @@ class SelectorQuery {
invokeMethod('requestComponentInfo', this._page, this._queue, res => { invokeMethod('requestComponentInfo', this._page, this._queue, res => {
const queueCbs = this._queueCb const queueCbs = this._queueCb
res.forEach((result, index) => { res.forEach((result, index) => {
if (Array.isArray(result)) {
result.forEach(convertContext)
} else {
convertContext(result)
}
const queueCb = queueCbs[index] const queueCb = queueCbs[index]
if (isFn(queueCb)) { if (isFn(queueCb)) {
queueCb.call(this, result) queueCb.call(this, result)
...@@ -109,4 +146,4 @@ export function createSelectorQuery (context) { ...@@ -109,4 +146,4 @@ export function createSelectorQuery (context) {
return new SelectorQuery(context) return new SelectorQuery(context)
} }
return new SelectorQuery(getCurrentPageVm('createSelectorQuery')) return new SelectorQuery(getCurrentPageVm('createSelectorQuery'))
} }
...@@ -74,6 +74,11 @@ function getNodeInfo (el, fields) { ...@@ -74,6 +74,11 @@ function getNodeInfo (el, fields) {
info.scrollTop = 0 info.scrollTop = 0
} }
} }
if (fields.context) {
if (el.__vue__ && el.__vue__._getContextInfo) {
info.context = el.__vue__._getContextInfo()
}
}
return info return info
} }
...@@ -134,4 +139,4 @@ export function requestComponentInfo ({ ...@@ -134,4 +139,4 @@ export function requestComponentInfo ({
reqId, reqId,
res: result res: result
}, pageVm.$page.id) }, pageVm.$page.id)
} }
.ql-container {
display: block;
position: relative;
box-sizing: border-box;
-webkit-user-select: text;
user-select: text;
outline: none;
overflow: hidden;
width: 100%;
height: 200px;
min-height: 200px;
}
.ql-container[hidden] {
display: none;
}
.ql-container .ql-editor {
position: relative;
font-size: inherit;
line-height: inherit;
font-family: inherit;
min-height: inherit;
width: 100%;
height: 100%;
padding: 0;
overflow-x: hidden;
overflow-y: auto;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-overflow-scrolling: touch;
}
.ql-container .ql-editor::-webkit-scrollbar {
width: 0 !important;
}
.ql-container .ql-editor.scroll-disabled {
overflow: hidden;
}
.ql-container .ql-image-overlay {
display: flex;
position: absolute;
box-sizing: border-box;
border: 1px dashed #ccc;
justify-content: center;
align-items: center;
-webkit-user-select: none;
user-select: none;
}
.ql-container .ql-image-overlay .ql-image-size {
position: absolute;
padding: 4px 8px;
text-align: center;
background-color: #fff;
color: #888;
border: 1px solid #ccc;
box-sizing: border-box;
opacity: 0.8;
right: 4px;
top: 4px;
font-size: 12px;
display: inline-block;
width: auto;
}
.ql-container .ql-image-overlay .ql-image-toolbar {
position: relative;
text-align: center;
box-sizing: border-box;
background: #000;
border-radius: 5px;
color: #fff;
font-size: 0;
min-height: 24px;
z-index: 100;
}
.ql-container .ql-image-overlay .ql-image-toolbar span {
display: inline-block;
cursor: pointer;
padding: 5px;
font-size: 12px;
border-right: 1px solid #fff;
}
.ql-container .ql-image-overlay .ql-image-toolbar span:last-child {
border-right: 0;
}
.ql-container .ql-image-overlay .ql-image-toolbar span.triangle-up {
padding: 0;
position: absolute;
top: -12px;
left: 50%;
transform: translatex(-50%);
width: 0;
height: 0;
border-width: 6px;
border-style: solid;
border-color: transparent transparent black transparent;
}
.ql-container .ql-image-overlay .ql-image-handle {
position: absolute;
height: 12px;
width: 12px;
border-radius: 50%;
border: 1px solid #ccc;
box-sizing: border-box;
background: #fff;
}
.ql-container img {
display: inline-block;
max-width: 100%;
}
.ql-clipboard p {
margin: 0;
padding: 0;
}
.ql-editor {
box-sizing: border-box;
height: 100%;
outline: none;
overflow-y: auto;
tab-size: 4;
-moz-tab-size: 4;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.ql-editor > * {
cursor: text;
}
.ql-editor p,
.ql-editor ol,
.ql-editor ul,
.ql-editor pre,
.ql-editor blockquote,
.ql-editor h1,
.ql-editor h2,
.ql-editor h3,
.ql-editor h4,
.ql-editor h5,
.ql-editor h6 {
margin: 0;
padding: 0;
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol > li,
.ql-editor ul > li {
list-style-type: none;
}
.ql-editor ul > li::before {
content: '\2022';
}
.ql-editor ul[data-checked=true],
.ql-editor ul[data-checked=false] {
pointer-events: none;
}
.ql-editor ul[data-checked=true] > li *,
.ql-editor ul[data-checked=false] > li * {
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before,
.ql-editor ul[data-checked=false] > li::before {
color: #777;
cursor: pointer;
pointer-events: all;
}
.ql-editor ul[data-checked=true] > li::before {
content: '\2611';
}
.ql-editor ul[data-checked=false] > li::before {
content: '\2610';
}
.ql-editor li::before {
display: inline-block;
white-space: nowrap;
width: 2em;
}
.ql-editor ol li {
counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
counter-increment: list-0;
}
.ql-editor ol li:before {
content: counter(list-0, decimal) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-increment: list-1;
}
.ql-editor ol li.ql-indent-1:before {
content: counter(list-1, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-1 {
counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-2 {
counter-increment: list-2;
}
.ql-editor ol li.ql-indent-2:before {
content: counter(list-2, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-2 {
counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-3 {
counter-increment: list-3;
}
.ql-editor ol li.ql-indent-3:before {
content: counter(list-3, decimal) '. ';
}
.ql-editor ol li.ql-indent-3 {
counter-reset: list-4 list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-4 {
counter-increment: list-4;
}
.ql-editor ol li.ql-indent-4:before {
content: counter(list-4, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-4 {
counter-reset: list-5 list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-5 {
counter-increment: list-5;
}
.ql-editor ol li.ql-indent-5:before {
content: counter(list-5, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-5 {
counter-reset: list-6 list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-6 {
counter-increment: list-6;
}
.ql-editor ol li.ql-indent-6:before {
content: counter(list-6, decimal) '. ';
}
.ql-editor ol li.ql-indent-6 {
counter-reset: list-7 list-8 list-9;
}
.ql-editor ol li.ql-indent-7 {
counter-increment: list-7;
}
.ql-editor ol li.ql-indent-7:before {
content: counter(list-7, lower-alpha) '. ';
}
.ql-editor ol li.ql-indent-7 {
counter-reset: list-8 list-9;
}
.ql-editor ol li.ql-indent-8 {
counter-increment: list-8;
}
.ql-editor ol li.ql-indent-8:before {
content: counter(list-8, lower-roman) '. ';
}
.ql-editor ol li.ql-indent-8 {
counter-reset: list-9;
}
.ql-editor ol li.ql-indent-9 {
counter-increment: list-9;
}
.ql-editor ol li.ql-indent-9:before {
content: counter(list-9, decimal) '. ';
}
.ql-editor .ql-indent-1:not(.ql-direction-rtl) {
padding-left: 2em;
}
.ql-editor li.ql-indent-1:not(.ql-direction-rtl) {
padding-left: 2em;
}
.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 2em;
}
.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right {
padding-right: 2em;
}
.ql-editor .ql-indent-2:not(.ql-direction-rtl) {
padding-left: 4em;
}
.ql-editor li.ql-indent-2:not(.ql-direction-rtl) {
padding-left: 4em;
}
.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 4em;
}
.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right {
padding-right: 4em;
}
.ql-editor .ql-indent-3:not(.ql-direction-rtl) {
padding-left: 6em;
}
.ql-editor li.ql-indent-3:not(.ql-direction-rtl) {
padding-left: 6em;
}
.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 6em;
}
.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right {
padding-right: 6em;
}
.ql-editor .ql-indent-4:not(.ql-direction-rtl) {
padding-left: 8em;
}
.ql-editor li.ql-indent-4:not(.ql-direction-rtl) {
padding-left: 8em;
}
.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 8em;
}
.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right {
padding-right: 8em;
}
.ql-editor .ql-indent-5:not(.ql-direction-rtl) {
padding-left: 10em;
}
.ql-editor li.ql-indent-5:not(.ql-direction-rtl) {
padding-left: 10em;
}
.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 10em;
}
.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right {
padding-right: 10em;
}
.ql-editor .ql-indent-6:not(.ql-direction-rtl) {
padding-left: 12em;
}
.ql-editor li.ql-indent-6:not(.ql-direction-rtl) {
padding-left: 12em;
}
.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 12em;
}
.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right {
padding-right: 12em;
}
.ql-editor .ql-indent-7:not(.ql-direction-rtl) {
padding-left: 14em;
}
.ql-editor li.ql-indent-7:not(.ql-direction-rtl) {
padding-left: 14em;
}
.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 14em;
}
.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right {
padding-right: 14em;
}
.ql-editor .ql-indent-8:not(.ql-direction-rtl) {
padding-left: 16em;
}
.ql-editor li.ql-indent-8:not(.ql-direction-rtl) {
padding-left: 16em;
}
.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 16em;
}
.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right {
padding-right: 16em;
}
.ql-editor .ql-indent-9:not(.ql-direction-rtl) {
padding-left: 18em;
}
.ql-editor li.ql-indent-9:not(.ql-direction-rtl) {
padding-left: 18em;
}
.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 18em;
}
.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right {
padding-right: 18em;
}
.ql-editor .ql-direction-rtl {
direction: rtl;
text-align: inherit;
}
.ql-editor .ql-align-center {
text-align: center;
}
.ql-editor .ql-align-justify {
text-align: justify;
}
.ql-editor .ql-align-right {
text-align: right;
}
.ql-editor.ql-blank::before {
color: rgba(0, 0, 0, 0.6);
content: attr(data-placeholder);
font-style: italic;
pointer-events: none;
position: absolute;
}
.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before {
pointer-events: none;
}
.ql-clipboard {
left: -100000px;
height: 1px;
overflow-y: hidden;
position: absolute;
top: 50%;
}
export default function (Quill) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.BLOCK,
whitelist: ['left', 'right', 'center', 'justify']
}
const AlignStyle = new Attributor.Style('align', 'text-align', config)
return {
'formats/align': AlignStyle
}
}
export default function (Quill) {
const { Scope } = Quill.import('parchment')
const BackgroundStyle = Quill.import('formats/background')
const BackgroundColorStyle = new BackgroundStyle.constructor('backgroundColor', 'background-color', {
scope: Scope.INLINE
})
return {
'formats/backgroundColor': BackgroundColorStyle
}
}
import { kebabCase } from 'uni-shared'
export default function (Quill) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.BLOCK
}
const margin = ['margin', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight']
const padding = ['padding', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight']
const result = {}
margin.concat(padding).forEach(name => {
result[`formats/${name}`] = new Attributor.Style(name, kebabCase(name), config)
})
return result
}
export default function (Quill) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.BLOCK,
whitelist: ['rtl']
}
const DirectionStyle = new Attributor.Style('direction', 'direction', config)
return {
'formats/direction': DirectionStyle
}
}
export default function (Quill) {
const BlockEmbed = Quill.import('blots/block/embed')
class Divider extends BlockEmbed { }
Divider.blotName = 'divider'
Divider.tagName = 'HR'
return {
'formats/divider': Divider
}
}
import { kebabCase } from 'uni-shared'
export default function (Quill) {
const { Scope, Attributor } = Quill.import('parchment')
const config = {
scope: Scope.INLINE
}
const font = ['font', 'fontSize', 'fontStyle', 'fontVariant', 'fontWeight', 'fontFamily']
const result = {}
font.forEach(name => {
result[`formats/${name}`] = new Attributor.Style(name, kebabCase(name), config)
})
return result
}
export default function (Quill) {
const Image = Quill.import('formats/image')
Image.sanitize = url => url
}
import divider from './divider'
import ins from './ins'
import align from './align'
import direction from './direction'
import list from './list'
import background from './background'
import box from './box'
import font from './font'
import text from './text'
import image from './image'
export function register (Quill) {
const formats = {
divider,
ins,
align,
direction,
list,
background,
box,
font,
text,
image
}
const options = {}
Object.values(formats).forEach(value => Object.assign(options, value(Quill)))
Quill.register(options, true)
}
export default function (Quill) {
const Inline = Quill.import('blots/inline')
class Ins extends Inline { }
Ins.blotName = 'ins'
Ins.tagName = 'INS'
return {
'formats/ins': Ins
}
}
export default function (Quill) {
const Parchment = Quill.import('parchment')
const Container = Quill.import('blots/container')
const ListItem = Quill.import('formats/list/item')
class List extends Container {
static create (value) {
let tagName = value === 'ordered' ? 'OL' : 'UL'
let node = super.create(tagName)
if (value === 'checked' || value === 'unchecked') {
node.setAttribute('data-checked', value === 'checked')
}
return node
}
static formats (domNode) {
if (domNode.tagName === 'OL') return 'ordered'
if (domNode.tagName === 'UL') {
if (domNode.hasAttribute('data-checked')) {
return domNode.getAttribute('data-checked') === 'true' ? 'checked' : 'unchecked'
} else {
return 'bullet'
}
}
return undefined
}
constructor (domNode) {
super(domNode)
const listEventHandler = (e) => {
if (e.target.parentNode !== domNode) return
let format = this.statics.formats(domNode)
let blot = Parchment.find(e.target)
if (format === 'checked') {
blot.format('list', 'unchecked')
} else if (format === 'unchecked') {
blot.format('list', 'checked')
}
}
domNode.addEventListener('click', listEventHandler)
}
format (name, value) {
if (this.children.length > 0) {
this.children.tail.format(name, value)
}
}
formats () {
// We don't inherit from FormatBlot
return { [this.statics.blotName]: this.statics.formats(this.domNode) }
}
insertBefore (blot, ref) {
if (blot instanceof ListItem) {
super.insertBefore(blot, ref)
} else {
let index = ref == null ? this.length() : ref.offset(this)
let after = this.split(index)
after.parent.insertBefore(blot, after)
}
}
optimize (context) {
super.optimize(context)
let next = this.next
if (next != null && next.prev === this &&
next.statics.blotName === this.statics.blotName &&
next.domNode.tagName === this.domNode.tagName &&
next.domNode.getAttribute('data-checked') === this.domNode.getAttribute('data-checked')) {
next.moveChildren(this)
next.remove()
}
}
replace (target) {
if (target.statics.blotName !== this.statics.blotName) {
let item = Parchment.create(this.statics.defaultChild)
target.moveChildren(item)
this.appendChild(item)
}
super.replace(target)
}
}
List.blotName = 'list'
List.scope = Parchment.Scope.BLOCK_BLOT
List.tagName = ['OL', 'UL']
List.defaultChild = 'list-item'
List.allowedChildren = [ListItem]
return {
'formats/list': List
}
}
import { kebabCase } from 'uni-shared'
export default function (Quill) {
const { Scope, Attributor } = Quill.import('parchment')
const text = [{
name: 'lineHeight',
scope: Scope.BLOCK
}, {
name: 'letterSpacing',
scope: Scope.INLINE
}, {
name: 'textDecoration',
scope: Scope.INLINE
}, {
name: 'textIndent',
scope: Scope.BLOCK
}]
const result = {}
text.forEach(({ name, scope }) => {
result[`formats/${name}`] = new Attributor.Style(name, kebabCase(name), {
scope
})
})
return result
}
<template>
<uni-editor
:id="id"
class="ql-container" />
</template>
<script>
import {
subscriber,
emitter,
keyboard
} from 'uni-mixins'
import HTMLParser from 'uni-helpers/html-parser'
import * as formats from './formats'
export default {
name: 'Editor',
mixins: [subscriber, emitter, keyboard],
props: {
id: {
type: String,
default: ''
},
readOnly: {
type: [Boolean, String],
default: false
},
placeholder: {
type: String,
default: ''
},
showImgSize: {
type: [Boolean, String],
default: false
},
showImgToolbar: {
type: [Boolean, String],
default: false
},
showImgResize: {
type: [Boolean, String],
default: false
}
},
data () {
return {
quillReady: false
}
},
computed: {
},
watch: {
readOnly (value) {
if (this.quillReady) {
const quill = this.quill
quill.enable(!value)
if (!value) {
quill.blur()
}
}
},
placeholder (value) {
if (this.quillReady) {
this.quill.root.setAttribute('data-placeholder', value)
}
}
},
mounted () {
const imageResizeModules = []
if (this.showImgSize) {
imageResizeModules.push('DisplaySize')
}
if (this.showImgToolbar) {
imageResizeModules.push('Toolbar')
}
if (this.showImgResize) {
imageResizeModules.push('Resize')
}
this.loadQuill(() => {
if (imageResizeModules.length) {
this.loadImageResizeModule(() => {
this.initQuill(imageResizeModules)
})
} else {
this.initQuill(imageResizeModules)
}
})
},
methods: {
_handleSubscribe ({
type,
data
}) {
const { options, callbackId } = data
const quill = this.quill
const Quill = window.Quill
let res
let range
let errMsg
if (this.quillReady) {
switch (type) {
case 'format':
let { name = '', value = false } = options
range = quill.getSelection(true)
let format = quill.getFormat(range)[name] || false
if (['bold', 'italic', 'underline', 'strike', 'ins'].includes(name)) {
value = !format
} else if (name === 'direction') {
value = value === 'rtl' && format ? false : value
const align = quill.getFormat(range).align
if (value === 'rtl' && !align) {
quill.format('align', 'right', Quill.sources.USER)
} else if (!value && align === 'right') {
quill.format('align', false, Quill.sources.USER)
}
} else if (name === 'indent') {
const rtl = quill.getFormat(range).direction === 'rtl'
if (rtl) {
value = !value
}
value = value ? '+1' : '-1'
} else {
if (name === 'list') {
value = value === 'check' ? 'unchecked' : value
format = format === 'checked' ? 'unchecked' : format
}
value = ((format && format !== (value || false)) || (!format && value)) ? value : !format
}
quill.format(name, value, Quill.sources.USER)
break
case 'insertDivider':
range = quill.getSelection(true)
quill.insertText(range.index, '\n', Quill.sources.USER)
quill.insertEmbed(range.index + 1, 'divider', true, Quill.sources.USER)
quill.setSelection(range.index + 2, Quill.sources.SILENT)
break
case 'insertImage':
range = quill.getSelection(true)
const { src = '', alt = '', data = {} } = options
quill.insertEmbed(range.index, 'image', this.$getRealPath(src), Quill.sources.USER)
quill.formatText(range.index, 1, 'alt', alt)
quill.formatText(range.index, 1, 'data-custom', Object.keys(data).map(key => `${key}=${data[key]}`).join('&'))
quill.setSelection(range.index + 1, Quill.sources.SILENT)
break
case 'insertText':
range = quill.getSelection(true)
const { text = '' } = options
quill.insertText(range.index, text, Quill.sources.USER)
quill.setSelection(range.index + text.length, 0, Quill.sources.SILENT)
break
case 'setContents':
const { delta, html } = options
if (typeof delta === 'object') {
quill.setContents(delta, Quill.sources.SILENT)
} else if (typeof html === 'string') {
quill.setContents(this.html2delta(html), Quill.sources.SILENT)
} else {
errMsg = 'contents is missing'
}
break
case 'getContents':
res = this.getContents()
break
case 'clear':
quill.setContents([])
break
case 'removeFormat':
range = quill.getSelection(true)
var parchment = Quill.import('parchment')
if (range.length) {
quill.removeFormat(range, Quill.sources.USER)
} else {
Object.keys(quill.getFormat(range)).forEach(key => {
if (parchment.query(key, parchment.Scope.INLINE)) {
quill.format(key, false)
}
})
}
break
case 'undo':
quill.history.undo()
break
case 'redo':
quill.history.redo()
break
default:
break
}
} else {
errMsg = 'not ready'
}
if (callbackId) {
UniViewJSBridge.publishHandler('onEditorMethodCallback', {
callbackId,
data: Object.assign({}, res, {
errMsg: `${type}:${errMsg ? 'fail ' + errMsg : 'ok'}`
})
}, this.$page.id)
}
},
loadQuill (callback) {
if (typeof window.Quill === 'function') {
if (typeof callback === 'function') {
callback()
}
return
}
let script = document.createElement('script')
script.src = window.plus ? './__uniappquill.js' : 'https://unpkg.com/quill@1.3.7/dist/quill.min.js'
document.body.appendChild(script)
script.onload = callback
},
loadImageResizeModule (callback) {
if (typeof window.ImageResize === 'function') {
if (typeof callback === 'function') {
callback()
}
return
}
let script = document.createElement('script')
script.src = window.plus ? './__uniappquillimageresize.js' : 'https://unpkg.com/quill-image-resize-mp@3.0.1/image-resize.min.js'
document.body.appendChild(script)
script.onload = callback
},
initQuill (imageResizeModules) {
const Quill = window.Quill
Quill.register('modules/ImageResize', window.ImageResize.default)
formats.register(Quill)
const quill = this.quill = new Quill(this.$el, {
toolbar: false,
readOnly: this.readOnly,
placeholder: this.placeholder,
modules: {
ImageResize: {
modules: imageResizeModules
}
}
})
const $el = quill.root
const events = ['focus', 'blur']
events.forEach(name => {
$el.addEventListener(name, ($event) => {
this.$trigger(name, $event, this.getContents())
})
})
quill.on(Quill.events.TEXT_CHANGE, () => {
this.$trigger('input', {}, this.getContents())
})
quill.clipboard.addMatcher(Node.ELEMENT_NODE, (node, delta) => {
if (this.skipMatcher) {
return delta
}
return {
ops: delta.ops.filter(({ insert }) => typeof insert === 'string').map(({ insert }) => ({ insert }))
}
})
this.initKeyboard($el)
this.quillReady = true
this.$trigger('ready', event, {})
},
getContents () {
const quill = this.quill
const html = quill.root.innerHTML
const text = quill.getText()
const delta = quill.getContents()
return {
html,
text,
delta
}
},
html2delta (html) {
const tags = ['span', 'strong', 'b', 'ins', 'em', 'i', 'u', 'a', 'del', 's', 'sub', 'sup', 'img', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'ol', 'ul', 'li']
let content = ''
let disable
HTMLParser(html, {
start: function (tag, attrs, unary) {
if (!tags.includes(tag)) {
disable = !unary
return
}
disable = false
const arrts = attrs.map(({ name, value }) => `${name}="${value}"`).join(' ')
const start = `<${tag} ${arrts} ${unary ? '/' : ''}>`
content += start
},
end: function (tag) {
if (!disable) {
content += `</${tag}>`
}
},
chars: function (text) {
if (!disable) {
content += text
}
}
})
this.skipMatcher = true
const delta = this.quill.clipboard.convert(content)
this.skipMatcher = false
return delta
}
}
}
</script>
<style src="./editor.css"></style>
<style>
</style>
...@@ -19,6 +19,9 @@ export default { ...@@ -19,6 +19,9 @@ export default {
}, },
beforeDestroy () { // 销毁时移除 beforeDestroy () { // 销毁时移除
this._toggleListeners('unsubscribe', this.id) this._toggleListeners('unsubscribe', this.id)
if (this._contextId) {
this._toggleListeners('unsubscribe', this._contextId)
}
}, },
methods: { methods: {
_toggleListeners (type, id, watch) { _toggleListeners (type, id, watch) {
...@@ -31,6 +34,18 @@ export default { ...@@ -31,6 +34,18 @@ export default {
} }
// 纠正VUniVideo等组件命名为Video // 纠正VUniVideo等组件命名为Video
UniViewJSBridge[type](this.$page.id + '-' + this.$options.name.replace(/VUni([A-Z])/, '$1').toLowerCase() + '-' + id, this._handleSubscribe) UniViewJSBridge[type](this.$page.id + '-' + this.$options.name.replace(/VUni([A-Z])/, '$1').toLowerCase() + '-' + id, this._handleSubscribe)
},
_getContextInfo () {
const id = `context-${this._uid}`
if (!this._contextId) {
this._toggleListeners('subscribe', id)
this._contextId = id
}
return {
name: this.$options.name.replace(/VUni([A-Z])/, '$1').toLowerCase(),
id,
page: this.$page.id
}
} }
} }
} }
let id = 0
const callbacks = {}
function warp (fn) {
return function (options = {}) {
const callbackId = String(id++)
callbacks[callbackId] = {
success: options.success,
fail: options.fail,
complete: options.complete
}
const data = Object.assign({}, options)
delete data.success
delete data.fail
delete data.complete
const res = fn.bind(this)(data, callbackId)
if (res) {
invoke(callbackId, res)
}
}
}
function invoke (callbackId, res) {
const callback = callbacks[callbackId] || {}
delete callbacks[callbackId]
const errMsg = res.errMsg || ''
if (new RegExp('\\:\\s*fail').test(errMsg)) {
callback.fail && callback.fail(res)
} else {
callback.success && callback.success(res)
}
callback.complete && callback.complete(res)
}
export const callback = {
warp,
invoke
}
...@@ -4,3 +4,4 @@ export * from './color' ...@@ -4,3 +4,4 @@ export * from './color'
export * from './query' export * from './query'
export * from './scroll' export * from './scroll'
export * from './platform' export * from './platform'
export * from './callback'
...@@ -21,7 +21,7 @@ export function hasOwn (obj, key) { ...@@ -21,7 +21,7 @@ export function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key) return hasOwnProperty.call(obj, key)
} }
export function noop () {} export function noop () { }
export function toRawType (val) { export function toRawType (val) {
return _toString.call(val).slice(8, -1) return _toString.call(val).slice(8, -1)
...@@ -87,4 +87,8 @@ export function debounce (fn, delay) { ...@@ -87,4 +87,8 @@ export function debounce (fn, delay) {
const timerFn = () => fn.apply(this, arguments) const timerFn = () => fn.apply(this, arguments)
timeout = setTimeout(timerFn, delay) timeout = setTimeout(timerFn, delay)
} }
} }
export function kebabCase (string) {
return string.replace(/[A-Z]/g, str => '-' + str.toLowerCase())
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册