mdui.editor
Version:
Material Design 样式的富文本编辑器
1,852 lines (1,797 loc) • 124 kB
JavaScript
/*!
* mdui.editor 1.0.2 (https://github.com/zdhxiong/mdui.editor#readme)
* Copyright 2019-2020 zdhxiong
* Licensed under MIT
*/
function isFunction(target) {
return typeof target === 'function';
}
function isString(target) {
return typeof target === 'string';
}
function isNumber(target) {
return typeof target === 'number';
}
function isBoolean(target) {
return typeof target === 'boolean';
}
function isUndefined(target) {
return typeof target === 'undefined';
}
function isNull(target) {
return target === null;
}
function isWindow(target) {
return target instanceof Window;
}
function isDocument(target) {
return target instanceof Document;
}
function isElement(target) {
return target instanceof Element;
}
function isNode(target) {
return target instanceof Node;
}
/**
* 是否是 IE 浏览器
*/
function isIE() {
// @ts-ignore
return !!window.document.documentMode;
}
function isArrayLike(target) {
if (isFunction(target) || isWindow(target)) {
return false;
}
return isNumber(target.length);
}
function isObjectLike(target) {
return typeof target === 'object' && target !== null;
}
function toElement(target) {
return isDocument(target) ? target.documentElement : target;
}
/**
* 把用 - 分隔的字符串转为驼峰(如 box-sizing 转换为 boxSizing)
* @param string
*/
function toCamelCase(string) {
return string
.replace(/^-ms-/, 'ms-')
.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
}
/**
* 把驼峰法转为用 - 分隔的字符串(如 boxSizing 转换为 box-sizing)
* @param string
*/
function toKebabCase(string) {
return string.replace(/[A-Z]/g, (replacer) => '-' + replacer.toLowerCase());
}
/**
* 获取元素的样式值
* @param element
* @param name
*/
function getComputedStyleValue(element, name) {
return window.getComputedStyle(element).getPropertyValue(toKebabCase(name));
}
/**
* 检查元素的 box-sizing 是否是 border-box
* @param element
*/
function isBorderBox(element) {
return getComputedStyleValue(element, 'box-sizing') === 'border-box';
}
/**
* 获取元素的 padding, border, margin 宽度(两侧宽度的和,单位为px)
* @param element
* @param direction
* @param extra
*/
function getExtraWidth(element, direction, extra) {
const position = direction === 'width' ? ['Left', 'Right'] : ['Top', 'Bottom'];
return [0, 1].reduce((prev, _, index) => {
let prop = extra + position[index];
if (extra === 'border') {
prop += 'Width';
}
return prev + parseFloat(getComputedStyleValue(element, prop) || '0');
}, 0);
}
/**
* 获取元素的样式值,对 width 和 height 进行过处理
* @param element
* @param name
*/
function getStyle(element, name) {
// width、height 属性使用 getComputedStyle 得到的值不准确,需要使用 getBoundingClientRect 获取
if (name === 'width' || name === 'height') {
const valueNumber = element.getBoundingClientRect()[name];
if (isBorderBox(element)) {
return `${valueNumber}px`;
}
return `${valueNumber -
getExtraWidth(element, name, 'border') -
getExtraWidth(element, name, 'padding')}px`;
}
return getComputedStyleValue(element, name);
}
/**
* 获取子节点组成的数组
* @param target
* @param parent
*/
function getChildNodesArray(target, parent) {
const tempParent = document.createElement(parent);
tempParent.innerHTML = target;
return [].slice.call(tempParent.childNodes);
}
/**
* 始终返回 false 的函数
*/
function returnFalse() {
return false;
}
/**
* 数值单位的 CSS 属性
*/
const cssNumber = [
'animationIterationCount',
'columnCount',
'fillOpacity',
'flexGrow',
'flexShrink',
'fontWeight',
'gridArea',
'gridColumn',
'gridColumnEnd',
'gridColumnStart',
'gridRow',
'gridRowEnd',
'gridRowStart',
'lineHeight',
'opacity',
'order',
'orphans',
'widows',
'zIndex',
'zoom',
];
function each(target, callback) {
if (isArrayLike(target)) {
for (let i = 0; i < target.length; i += 1) {
if (callback.call(target[i], i, target[i]) === false) {
return target;
}
}
}
else {
const keys = Object.keys(target);
for (let i = 0; i < keys.length; i += 1) {
if (callback.call(target[keys[i]], keys[i], target[keys[i]]) === false) {
return target;
}
}
}
return target;
}
/**
* 为了使用模块扩充,这里不能使用默认导出
*/
class JQ {
constructor(arr) {
this.length = 0;
if (!arr) {
return this;
}
each(arr, (i, item) => {
// @ts-ignore
this[i] = item;
});
this.length = arr.length;
return this;
}
}
function get$() {
const $ = function (selector) {
if (!selector) {
return new JQ();
}
// JQ
if (selector instanceof JQ) {
return selector;
}
// function
if (isFunction(selector)) {
if (/complete|loaded|interactive/.test(document.readyState) &&
document.body) {
selector.call(document, $);
}
else {
document.addEventListener('DOMContentLoaded', () => selector.call(document, $), false);
}
return new JQ([document]);
}
// String
if (isString(selector)) {
const html = selector.trim();
// 根据 HTML 字符串创建 JQ 对象
if (html[0] === '<' && html[html.length - 1] === '>') {
let toCreate = 'div';
const tags = {
li: 'ul',
tr: 'tbody',
td: 'tr',
th: 'tr',
tbody: 'table',
option: 'select',
};
each(tags, (childTag, parentTag) => {
if (html.indexOf(`<${childTag}`) === 0) {
toCreate = parentTag;
return false;
}
return;
});
return new JQ(getChildNodesArray(html, toCreate));
}
// 根据 CSS 选择器创建 JQ 对象
const isIdSelector = selector[0] === '#' && !selector.match(/[ .<>:~]/);
if (!isIdSelector) {
return new JQ(document.querySelectorAll(selector));
}
const element = document.getElementById(selector.slice(1));
if (element) {
return new JQ([element]);
}
return new JQ();
}
if (isArrayLike(selector) && !isNode(selector)) {
return new JQ(selector);
}
return new JQ([selector]);
};
$.fn = JQ.prototype;
return $;
}
const $ = get$();
function extend(target, object1, ...objectN) {
objectN.unshift(object1);
each(objectN, (_, object) => {
each(object, (prop, value) => {
if (!isUndefined(value)) {
target[prop] = value;
}
});
});
return target;
}
$.fn.each = function (callback) {
return each(this, callback);
};
each(['add', 'remove', 'toggle'], (_, name) => {
$.fn[`${name}Class`] = function (className) {
if (name === 'remove' && !arguments.length) {
return this.each((_, element) => {
element.setAttribute('class', '');
});
}
return this.each((i, element) => {
if (!isElement(element)) {
return;
}
const classes = (isFunction(className)
? className.call(element, i, element.getAttribute('class') || '')
: className)
.split(' ')
.filter((name) => name);
each(classes, (_, cls) => {
element.classList[name](cls);
});
});
};
});
each(['insertBefore', 'insertAfter'], (nameIndex, name) => {
$.fn[name] = function (target) {
const $element = nameIndex ? $(this.get().reverse()) : this; // 顺序和 jQuery 保持一致
const $target = $(target);
const result = [];
$target.each((index, target) => {
if (!target.parentNode) {
return;
}
$element.each((_, element) => {
const newItem = index
? element.cloneNode(true)
: element;
const existingItem = nameIndex ? target.nextSibling : target;
result.push(newItem);
target.parentNode.insertBefore(newItem, existingItem);
});
});
return $(nameIndex ? result.reverse() : result);
};
});
/**
* 是否不是 HTML 字符串(包裹在 <> 中)
* @param target
*/
function isPlainText(target) {
return (isString(target) && (target[0] !== '<' || target[target.length - 1] !== '>'));
}
each(['before', 'after'], (nameIndex, name) => {
$.fn[name] = function (...args) {
// after 方法,多个参数需要按参数顺序添加到元素后面,所以需要将参数顺序反向处理
if (nameIndex === 1) {
args = args.reverse();
}
return this.each((index, element) => {
const targets = isFunction(args[0])
? [args[0].call(element, index, element.innerHTML)]
: args;
each(targets, (_, target) => {
let $target;
if (isPlainText(target)) {
$target = $(getChildNodesArray(target, 'div'));
}
else if (index && isElement(target)) {
$target = $(target.cloneNode(true));
}
else {
$target = $(target);
}
$target[nameIndex ? 'insertAfter' : 'insertBefore'](element);
});
});
};
});
function map(elements, callback) {
let value;
const ret = [];
each(elements, (i, element) => {
value = callback.call(window, element, i);
if (value != null) {
ret.push(value);
}
});
return [].concat(...ret);
}
$.fn.map = function (callback) {
return new JQ(map(this, (element, i) => callback.call(element, i, element)));
};
$.fn.clone = function () {
return this.map(function () {
return this.cloneNode(true);
});
};
$.fn.is = function (selector) {
let isMatched = false;
if (isFunction(selector)) {
this.each((index, element) => {
if (selector.call(element, index, element)) {
isMatched = true;
}
});
return isMatched;
}
if (isString(selector)) {
this.each((_, element) => {
if (isDocument(element) || isWindow(element)) {
return;
}
// @ts-ignore
const matches = element.matches || element.msMatchesSelector;
if (matches.call(element, selector)) {
isMatched = true;
}
});
return isMatched;
}
const $compareWith = $(selector);
this.each((_, element) => {
$compareWith.each((_, compare) => {
if (element === compare) {
isMatched = true;
}
});
});
return isMatched;
};
$.fn.remove = function (selector) {
return this.each((_, element) => {
if (element.parentNode && (!selector || $(element).is(selector))) {
element.parentNode.removeChild(element);
}
});
};
each(['prepend', 'append'], (nameIndex, name) => {
$.fn[name] = function (...args) {
return this.each((index, element) => {
const childNodes = element.childNodes;
const childLength = childNodes.length;
const child = childLength
? childNodes[nameIndex ? childLength - 1 : 0]
: document.createElement('div');
if (!childLength) {
element.appendChild(child);
}
let contents = isFunction(args[0])
? [args[0].call(element, index, element.innerHTML)]
: args;
// 如果不是字符串,则仅第一个元素使用原始元素,其他的都克隆自第一个元素
if (index) {
contents = contents.map((content) => {
return isString(content) ? content : $(content).clone();
});
}
$(child)[nameIndex ? 'after' : 'before'](...contents);
if (!childLength) {
element.removeChild(child);
}
});
};
});
each(['attr', 'prop', 'css'], (nameIndex, name) => {
function set(element, key, value) {
// 值为 undefined 时,不修改
if (isUndefined(value)) {
return;
}
switch (nameIndex) {
// attr
case 0:
if (isNull(value)) {
element.removeAttribute(key);
}
else {
element.setAttribute(key, value);
}
break;
// prop
case 1:
// @ts-ignore
element[key] = value;
break;
// css
default:
key = toCamelCase(key);
// @ts-ignore
element.style[key] = isNumber(value)
? `${value}${cssNumber.indexOf(key) > -1 ? '' : 'px'}`
: value;
break;
}
}
function get(element, key) {
switch (nameIndex) {
// attr
case 0:
// 属性不存在时,原生 getAttribute 方法返回 null,而 jquery 返回 undefined。这里和 jquery 保持一致
const value = element.getAttribute(key);
return isNull(value) ? undefined : value;
// prop
case 1:
// @ts-ignore
return element[key];
// css
default:
return getStyle(element, key);
}
}
$.fn[name] = function (key, value) {
if (isObjectLike(key)) {
each(key, (k, v) => {
// @ts-ignore
this[name](k, v);
});
return this;
}
if (arguments.length === 1) {
const element = this[0];
return isElement(element) ? get(element, key) : undefined;
}
return this.each((i, element) => {
set(element, key, isFunction(value) ? value.call(element, i, get(element, key)) : value);
});
};
});
/**
* 过滤掉数组中的重复元素
* @param arr 数组
* @example
```js
unique([1, 2, 12, 3, 2, 1, 2, 1, 1]);
// [1, 2, 12, 3]
```
*/
function unique(arr) {
const result = [];
each(arr, (_, val) => {
if (result.indexOf(val) === -1) {
result.push(val);
}
});
return result;
}
$.fn.children = function (selector) {
const children = [];
this.each((_, element) => {
each(element.childNodes, (__, childNode) => {
if (!isElement(childNode)) {
return;
}
if (!selector || $(childNode).is(selector)) {
children.push(childNode);
}
});
});
return new JQ(unique(children));
};
$.fn.slice = function (...args) {
return new JQ([].slice.apply(this, args));
};
$.fn.eq = function (index) {
const ret = index === -1 ? this.slice(index) : this.slice(index, +index + 1);
return new JQ(ret);
};
$.fn.first = function () {
return this.eq(0);
};
each(['val', 'html', 'text'], (nameIndex, name) => {
const props = {
0: 'value',
1: 'innerHTML',
2: 'textContent',
};
const propName = props[nameIndex];
function get($elements) {
// text() 获取所有元素的文本
if (nameIndex === 2) {
// @ts-ignore
return map($elements, (element) => toElement(element)[propName]).join('');
}
// 空集合时,val() 和 html() 返回 undefined
if (!$elements.length) {
return undefined;
}
// val() 和 html() 仅获取第一个元素的内容
const firstElement = $elements[0];
// select multiple 返回数组
if (nameIndex === 0 && $(firstElement).is('select[multiple]')) {
return map($(firstElement).find('option:checked'), (element) => element.value);
}
// @ts-ignore
return firstElement[propName];
}
function set(element, value) {
// text() 和 html() 赋值为 undefined,则保持原内容不变
// val() 赋值为 undefined 则赋值为空
if (isUndefined(value)) {
if (nameIndex !== 0) {
return;
}
value = '';
}
if (nameIndex === 1 && isElement(value)) {
value = value.outerHTML;
}
// @ts-ignore
element[propName] = value;
}
$.fn[name] = function (value) {
// 获取值
if (!arguments.length) {
return get(this);
}
// 设置值
return this.each((i, element) => {
const computedValue = isFunction(value)
? value.call(element, i, get($(element)))
: value;
// value 是数组,则选中数组中的元素,反选不在数组中的元素
if (nameIndex === 0 && Array.isArray(computedValue)) {
// select[multiple]
if ($(element).is('select[multiple]')) {
map($(element).find('option'), (option) => (option.selected =
computedValue.indexOf(option.value) >
-1));
}
// 其他 checkbox, radio 等元素
else {
element.checked =
computedValue.indexOf(element.value) > -1;
}
}
else {
set(element, computedValue);
}
});
};
});
$.fn.last = function () {
return this.eq(-1);
};
/**
* 检查 container 元素内是否包含 contains 元素
* @param container 父元素
* @param contains 子元素
* @example
```js
contains( document, document.body ); // true
contains( document.getElementById('test'), document ); // false
contains( $('.container').get(0), $('.contains').get(0) ); // false
```
*/
function contains(container, contains) {
return container !== contains && toElement(container).contains(contains);
}
/**
* 把第二个数组的元素追加到第一个数组中,并返回合并后的数组
* @param first 第一个数组
* @param second 该数组的元素将被追加到第一个数组中
* @example
```js
merge( [ 0, 1, 2 ], [ 2, 3, 4 ] )
// [ 0, 1, 2, 2, 3, 4 ]
```
*/
function merge(first, second) {
each(second, (_, value) => {
first.push(value);
});
return first;
}
$.fn.get = function (index) {
return index === undefined
? [].slice.call(this)
: this[index >= 0 ? index : index + this.length];
};
$.fn.find = function (selector) {
const foundElements = [];
this.each((_, element) => {
merge(foundElements, $(element.querySelectorAll(selector)).get());
});
return new JQ(foundElements);
};
// 存储事件
const handlers = {};
// 元素ID
let mduiElementId = 1;
/**
* 为元素赋予一个唯一的ID
*/
function getElementId(element) {
const key = '_mduiEventId';
// @ts-ignore
if (!element[key]) {
// @ts-ignore
element[key] = ++mduiElementId;
}
// @ts-ignore
return element[key];
}
/**
* 解析事件名中的命名空间
*/
function parse(type) {
const parts = type.split('.');
return {
type: parts[0],
ns: parts.slice(1).sort().join(' '),
};
}
/**
* 命名空间匹配规则
*/
function matcherFor(ns) {
return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)');
}
/**
* 获取匹配的事件
* @param element
* @param type
* @param func
* @param selector
*/
function getHandlers(element, type, func, selector) {
const event = parse(type);
return (handlers[getElementId(element)] || []).filter((handler) => handler &&
(!event.type || handler.type === event.type) &&
(!event.ns || matcherFor(event.ns).test(handler.ns)) &&
(!func || getElementId(handler.func) === getElementId(func)) &&
(!selector || handler.selector === selector));
}
/**
* 添加事件监听
* @param element
* @param types
* @param func
* @param data
* @param selector
*/
function add(element, types, func, data, selector) {
const elementId = getElementId(element);
if (!handlers[elementId]) {
handlers[elementId] = [];
}
// 传入 data.useCapture 来设置 useCapture: true
let useCapture = false;
if (isObjectLike(data) && data.useCapture) {
useCapture = true;
}
types.split(' ').forEach((type) => {
if (!type) {
return;
}
const event = parse(type);
function callFn(e, elem) {
// 因为鼠标事件模拟事件的 detail 属性是只读的,因此在 e._detail 中存储参数
const result = func.apply(elem,
// @ts-ignore
e._detail === undefined ? [e] : [e].concat(e._detail));
if (result === false) {
e.preventDefault();
e.stopPropagation();
}
}
function proxyFn(e) {
// @ts-ignore
if (e._ns && !matcherFor(e._ns).test(event.ns)) {
return;
}
// @ts-ignore
e._data = data;
if (selector) {
// 事件代理
$(element)
.find(selector)
.get()
.reverse()
.forEach((elem) => {
if (elem === e.target ||
contains(elem, e.target)) {
callFn(e, elem);
}
});
}
else {
// 不使用事件代理
callFn(e, element);
}
}
const handler = {
type: event.type,
ns: event.ns,
func,
selector,
id: handlers[elementId].length,
proxy: proxyFn,
};
handlers[elementId].push(handler);
element.addEventListener(handler.type, proxyFn, useCapture);
});
}
/**
* 移除事件监听
* @param element
* @param types
* @param func
* @param selector
*/
function remove(element, types, func, selector) {
const handlersInElement = handlers[getElementId(element)] || [];
const removeEvent = (handler) => {
delete handlersInElement[handler.id];
element.removeEventListener(handler.type, handler.proxy, false);
};
if (!types) {
handlersInElement.forEach((handler) => removeEvent(handler));
}
else {
types.split(' ').forEach((type) => {
if (type) {
getHandlers(element, type, func, selector).forEach((handler) => removeEvent(handler));
}
});
}
}
$.fn.off = function (types, selector, callback) {
// types 是对象
if (isObjectLike(types)) {
each(types, (type, fn) => {
// this.off('click', undefined, function () {})
// this.off('click', '.box', function () {})
this.off(type, selector, fn);
});
return this;
}
// selector 不存在
if (selector === false || isFunction(selector)) {
callback = selector;
selector = undefined;
// this.off('click', undefined, function () {})
}
// callback 传入 `false`,相当于 `return false`
if (callback === false) {
callback = returnFalse;
}
return this.each(function () {
remove(this, types, callback, selector);
});
};
$.fn.on = function (types, selector, data, callback, one) {
// types 可以是 type/func 对象
if (isObjectLike(types)) {
// (types-Object, selector, data)
if (!isString(selector)) {
// (types-Object, data)
data = data || selector;
selector = undefined;
}
each(types, (type, fn) => {
// selector 和 data 都可能是 undefined
// @ts-ignore
this.on(type, selector, data, fn, one);
});
return this;
}
if (data == null && callback == null) {
// (types, fn)
callback = selector;
data = selector = undefined;
}
else if (callback == null) {
if (isString(selector)) {
// (types, selector, fn)
callback = data;
data = undefined;
}
else {
// (types, data, fn)
callback = data;
data = selector;
selector = undefined;
}
}
if (callback === false) {
callback = returnFalse;
}
else if (!callback) {
return this;
}
// $().one()
if (one) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
const origCallback = callback;
callback = function (event) {
_this.off(event.type, selector, callback);
// eslint-disable-next-line prefer-rest-params
return origCallback.apply(this, arguments);
};
}
return this.each(function () {
add(this, types, callback, data, selector);
});
};
each(['appendTo', 'prependTo'], (nameIndex, name) => {
$.fn[name] = function (target) {
const extraChilds = [];
const $target = $(target).map((_, element) => {
const childNodes = element.childNodes;
const childLength = childNodes.length;
if (childLength) {
return childNodes[nameIndex ? 0 : childLength - 1];
}
const child = document.createElement('div');
element.appendChild(child);
extraChilds.push(child);
return child;
});
const $result = this[nameIndex ? 'insertBefore' : 'insertAfter']($target);
$(extraChilds).remove();
return $result;
};
});
class CommonAbstract {
/**
* @param editor 编辑器实例
*/
constructor(editor) {
this.editor = editor;
}
/**
* 工具栏 JQ 对象
*/
get $toolbar() {
return this.editor.$toolbar;
}
/**
* 内容区域 JQ 对象
*/
get $container() {
return this.editor.$container;
}
/**
* 选区实例
*/
get selection() {
return this.editor.selection;
}
/**
* 命令实例
*/
get command() {
return this.editor.command;
}
}
/**
* 封装 document.execCommand 命令
*/
class Command extends CommonAbstract {
/**
* 执行命令
* @param name
* @param value
*/
do(name, value) {
// 如果无选区,忽略
if (!this.selection.getRange()) {
return;
}
// 恢复选区
this.selection.restore();
const customName = name;
// 执行命令
// @ts-ignore
if (this[customName]) {
// @ts-ignore
this[customName](value);
}
else {
document.execCommand(name, false, value);
}
// 修改菜单状态
this.editor.menus.changeStatus();
// 最后,恢复选区保证光标在原来的位置闪烁
this.selection.saveRange();
this.selection.restore();
// 触发 onchange
if (this.editor.change) {
this.editor.change();
}
}
/**
* 自定义 insertHTML 事件,在当前选区中插入指定 HTML
* @param html
*/
// @ts-ignore
insertHTML(html) {
// W3C
if (document.queryCommandSupported('insertHTML')) {
document.execCommand('insertHTML', false, html);
return;
}
const range = this.selection.getRange();
if (range.insertNode) {
// IE
range.deleteContents();
range.insertNode($(html)[0]);
// @ts-ignore
}
else if (range.pasteHTML) {
// IE <= 10
// @ts-ignore
range.pasteHTML(html);
}
}
/**
* 用指定 HTML 替换当前选区的 root 元素
* @param html
*/
// @ts-ignore
replaceRoot(html) {
const $oldElem = this.selection.getRootElem();
const $newElem = $(html).insertAfter($oldElem);
$oldElem.remove();
this.selection.createRangeByElem($newElem, false, true);
this.selection.restore();
}
/**
* 在当前选区的 root 元素后面插入指定 html
* @param html
*/
// @ts-ignore
insertAfterRoot(html) {
const $oldElem = this.selection.getRootElem();
const $newElem = $(html).insertAfter($oldElem);
this.selection.createRangeByElem($newElem, false, true);
this.selection.restore();
}
/**
* 在当前 $content 的最后追加 html
* @param html
*/
// @ts-ignore
appendHTML(html) {
const $newElem = $(html).appendTo(this.$container);
this.selection.createRangeByElem($newElem, false, true);
this.selection.restore();
}
/**
* 插入 elem
* @param $elem
*/
// @ts-ignore
insertElem($elem) {
const range = this.selection.getRange();
if (range.insertNode) {
range.deleteContents();
range.insertNode($elem[0]);
}
}
}
class MenuAbstract extends CommonAbstract {
/**
* @param editor 编辑器实例
* @param $button 按钮 JQ 对象
*/
constructor(editor, $button) {
super(editor);
this.$button = $button;
}
/**
* 按钮是否激活
*/
isActive() {
return false;
}
}
/**
* 按钮图标
*/
MenuAbstract.icon = '';
/**
* 按钮名称
*/
MenuAbstract.title = '';
/**
* 激活按钮时,需要禁用的其他按钮
*/
MenuAbstract.disable = [];
/**
* 原生命令抽象类
*/
class MenuNativeAbstract extends MenuAbstract {
onclick() {
const isSelectionEmpty = this.selection.isEmpty();
if (isSelectionEmpty) {
// 选区是空的,插入并选中一个“空白”
this.selection.createEmptyRange(this.getElementName());
}
// 执行 bold 命令
this.command.do(this.getCommandName());
if (isSelectionEmpty) {
// 需要将选区折叠起来
this.selection.collapseRange();
this.selection.restore();
}
}
isActive() {
return document.queryCommandState(this.getCommandName());
}
}
/**
* 加粗
*/
class Bold extends MenuNativeAbstract {
getCommandName() {
return 'bold';
}
getElementName() {
return 'strong';
}
}
Bold.icon = 'format_bold';
Bold.title = '粗体';
Bold.disable = ['image'];
const $document = $(document);
const $window = $(window);
const $body = $('body');
// 避免页面加载完后直接执行css动画
// https://css-tricks.com/transitions-only-after-page-load/
setTimeout(() => $body.addClass('mdui-loaded'));
const mdui = {
$: $,
};
$.fn.hasClass = function (className) {
return this[0].classList.contains(className);
};
/**
* 值上面的 padding、border、margin 处理
* @param element
* @param name
* @param value
* @param funcIndex
* @param includeMargin
* @param multiply
*/
function handleExtraWidth(element, name, value, funcIndex, includeMargin, multiply) {
// 获取元素的 padding, border, margin 宽度(两侧宽度的和)
const getExtraWidthValue = (extra) => {
return (getExtraWidth(element, name.toLowerCase(), extra) *
multiply);
};
if (funcIndex === 2 && includeMargin) {
value += getExtraWidthValue('margin');
}
if (isBorderBox(element)) {
// IE 为 box-sizing: border-box 时,得到的值不含 border 和 padding,这里先修复
// 仅获取时需要处理,multiply === 1 为 get
if (isIE() && multiply === 1) {
value += getExtraWidthValue('border');
value += getExtraWidthValue('padding');
}
if (funcIndex === 0) {
value -= getExtraWidthValue('border');
}
if (funcIndex === 1) {
value -= getExtraWidthValue('border');
value -= getExtraWidthValue('padding');
}
}
else {
if (funcIndex === 0) {
value += getExtraWidthValue('padding');
}
if (funcIndex === 2) {
value += getExtraWidthValue('border');
value += getExtraWidthValue('padding');
}
}
return value;
}
/**
* 获取元素的样式值
* @param element
* @param name
* @param funcIndex 0: innerWidth, innerHeight; 1: width, height; 2: outerWidth, outerHeight
* @param includeMargin
*/
function get(element, name, funcIndex, includeMargin) {
const clientProp = `client${name}`;
const scrollProp = `scroll${name}`;
const offsetProp = `offset${name}`;
const innerProp = `inner${name}`;
// $(window).width()
if (isWindow(element)) {
// outerWidth, outerHeight 需要包含滚动条的宽度
return funcIndex === 2
? element[innerProp]
: toElement(document)[clientProp];
}
// $(document).width()
if (isDocument(element)) {
const doc = toElement(element);
return Math.max(
// @ts-ignore
element.body[scrollProp], doc[scrollProp],
// @ts-ignore
element.body[offsetProp], doc[offsetProp], doc[clientProp]);
}
const value = parseFloat(getComputedStyleValue(element, name.toLowerCase()) || '0');
return handleExtraWidth(element, name, value, funcIndex, includeMargin, 1);
}
/**
* 设置元素的样式值
* @param element
* @param elementIndex
* @param name
* @param funcIndex 0: innerWidth, innerHeight; 1: width, height; 2: outerWidth, outerHeight
* @param includeMargin
* @param value
*/
function set(element, elementIndex, name, funcIndex, includeMargin, value) {
let computedValue = isFunction(value)
? value.call(element, elementIndex, get(element, name, funcIndex, includeMargin))
: value;
if (computedValue == null) {
return;
}
const $element = $(element);
const dimension = name.toLowerCase();
// 特殊的值,不需要计算 padding、border、margin
if (['auto', 'inherit', ''].indexOf(computedValue) > -1) {
$element.css(dimension, computedValue);
return;
}
// 其他值保留原始单位。注意:如果不使用 px 作为单位,则算出的值一般是不准确的
const suffix = computedValue.toString().replace(/\b[0-9.]*/, '');
const numerical = parseFloat(computedValue);
computedValue =
handleExtraWidth(element, name, numerical, funcIndex, includeMargin, -1) +
(suffix || 'px');
$element.css(dimension, computedValue);
}
each(['Width', 'Height'], (_, name) => {
each([`inner${name}`, name.toLowerCase(), `outer${name}`], (funcIndex, funcName) => {
$.fn[funcName] = function (margin, value) {
// 是否是赋值操作
const isSet = arguments.length && (funcIndex < 2 || !isBoolean(margin));
const includeMargin = margin === true || value === true;
// 获取第一个元素的值
if (!isSet) {
return this.length
? get(this[0], name, funcIndex, includeMargin)
: undefined;
}
// 设置每个元素的值
return this.each((index, element) => set(element, index, name, funcIndex, includeMargin, margin));
};
});
});
$.fn.hide = function () {
return this.each(function () {
this.style.display = 'none';
});
};
const elementDisplay = {};
/**
* 获取元素的初始 display 值,用于 .show() 方法
* @param nodeName
*/
function defaultDisplay(nodeName) {
let element;
let display;
if (!elementDisplay[nodeName]) {
element = document.createElement(nodeName);
document.body.appendChild(element);
display = getStyle(element, 'display');
element.parentNode.removeChild(element);
if (display === 'none') {
display = 'block';
}
elementDisplay[nodeName] = display;
}
return elementDisplay[nodeName];
}
/**
* 显示指定元素
* @returns {JQ}
*/
$.fn.show = function () {
return this.each(function () {
if (this.style.display === 'none') {
this.style.display = '';
}
if (getStyle(this, 'display') === 'none') {
this.style.display = defaultDisplay(this.nodeName);
}
});
};
$.fn.transitionEnd = function (callback) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
const events = ['webkitTransitionEnd', 'transitionend'];
function fireCallback(e) {
if (e.target !== this) {
return;
}
// @ts-ignore
callback.call(this, e);
each(events, (_, event) => {
that.off(event, fireCallback);
});
}
each(events, (_, event) => {
that.on(event, fireCallback);
});
return this;
};
const dataNS = '_mduiElementDataStorage';
/**
* 在元素上设置键值对数据
* @param element
* @param object
*/
function setObjectToElement(element, object) {
// @ts-ignore
if (!element[dataNS]) {
// @ts-ignore
element[dataNS] = {};
}
each(object, (key, value) => {
// @ts-ignore
element[dataNS][toCamelCase(key)] = value;
});
}
function data(element, key, value) {
// 根据键值对设置值
// data(element, { 'key' : 'value' })
if (isObjectLike(key)) {
setObjectToElement(element, key);
return key;
}
// 根据 key、value 设置值
// data(element, 'key', 'value')
if (!isUndefined(value)) {
setObjectToElement(element, { [key]: value });
return value;
}
// 获取所有值
// data(element)
if (isUndefined(key)) {
// @ts-ignore
return element[dataNS] ? element[dataNS] : {};
}
// 从 dataNS 中获取指定值
// data(element, 'key')
key = toCamelCase(key);
// @ts-ignore
if (element[dataNS] && key in element[dataNS]) {
// @ts-ignore
return element[dataNS][key];
}
return undefined;
}
const rbrace = /^(?:{[\w\W]*\}|\[[\w\W]*\])$/;
// 从 `data-*` 中获取的值,需要经过该函数转换
function getData(value) {
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
if (value === 'null') {
return null;
}
if (value === +value + '') {
return +value;
}
if (rbrace.test(value)) {
return JSON.parse(value);
}
return value;
}
// 若 value 不存在,则从 `data-*` 中获取值
function dataAttr(element, key, value) {
if (isUndefined(value) && element.nodeType === 1) {
const name = 'data-' + toKebabCase(key);
value = element.getAttribute(name);
if (isString(value)) {
try {
value = getData(value);
}
catch (e) { }
}
else {
value = undefined;
}
}
return value;
}
$.fn.data = function (key, value) {
// 获取所有值
if (isUndefined(key)) {
if (!this.length) {
return undefined;
}
const element = this[0];
const resultData = data(element);
// window, document 上不存在 `data-*` 属性
if (element.nodeType !== 1) {
return resultData;
}
// 从 `data-*` 中获取值
const attrs = element.attributes;
let i = attrs.length;
while (i--) {
if (attrs[i]) {
let name = attrs[i].name;
if (name.indexOf('data-') === 0) {
name = toCamelCase(name.slice(5));
resultData[name] = dataAttr(element, name, resultData[name]);
}
}
}
return resultData;
}
// 同时设置多个值
if (isObjectLike(key)) {
return this.each(function () {
data(this, key);
});
}
// value 传入了 undefined
if (arguments.length === 2 && isUndefined(value)) {
return this;
}
// 设置值
if (!isUndefined(value)) {
return this.each(function () {
data(this, key, value);
});
}
// 获取值
if (!this.length) {
return undefined;
}
return dataAttr(this[0], key, data(this[0], key));
};
$.hideOverlay = function (force = false) {
const $overlay = $('.mdui-overlay');
if (!$overlay.length) {
return;
}
let level = force ? 1 : $overlay.data('_overlay_level');
if (level > 1) {
$overlay.data('_overlay_level', --level);
return;
}
$overlay
.data('_overlay_level', 0)
.removeClass('mdui-overlay-show')
.data('_overlay_is_deleted', true)
.transitionEnd(() => {
if ($overlay.data('_overlay_is_deleted')) {
$overlay.remove();
}
});
};
$.lockScreen = function () {
// 不直接把 body 设为 box-sizing: border-box,避免污染全局样式
const newBodyWidth = $body.width();
let level = $body.data('_lockscreen_level') || 0;
$body
.addClass('mdui-locked')
.width(newBodyWidth)
.data('_lockscreen_level', ++level);
};
$.fn.reflow = function () {
return this.each(function () {
return this.clientLeft;
});
};
$.showOverlay = function (zIndex) {
let $overlay = $('.mdui-overlay');
if ($overlay.length) {
$overlay.data('_overlay_is_deleted', false);
if (!isUndefined(zIndex)) {
$overlay.css('z-index', zIndex);
}
}
else {
if (isUndefined(zIndex)) {
zIndex = 2000;
}
$overlay = $('<div class="mdui-overlay">')
.appendTo(document.body)
.reflow()
.css('z-index', zIndex);
}
let level = $overlay.data('_overlay_level') || 0;
return $overlay.data('_overlay_level', ++level).addClass('mdui-overlay-show');
};
$.throttle = function (fn, delay = 16) {
let timer = null;
return function (...args) {
if (isNull(timer)) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
}
};
};
$.unlockScreen = function (force = false) {
let level = force ? 1 : $body.data('_lockscreen_level');
if (level > 1) {
$body.data('_lockscreen_level', --level);
return;
}
$body.data('_lockscreen_level', 0).removeClass('mdui-locked').width('');
};
$.fn.trigger = function (type, extraParameters) {
const event = parse(type);
let eventObject;
const eventParams = {
bubbles: true,
cancelable: true,
};
const isMouseEvent = ['click', 'mousedown', 'mouseup', 'mousemove'].indexOf(event.type) > -1;
if (isMouseEvent) {
// Note: MouseEvent 无法传入 detail 参数
eventObject = new MouseEvent(event.type, eventParams);
}
else {
eventParams.detail = extraParameters;
eventObject = new CustomEvent(event.type, eventParams);
}
// @ts-ignore
eventObject._detail = extraParameters;
// @ts-ignore
eventObject._ns = event.ns;
return this.each(function () {
this.dispatchEvent(eventObject);
});
};
/**
* 触发组件上的事件
* @param eventName 事件名
* @param componentName 组件名
* @param target 在该元素上触发事件
* @param instance 组件实例
* @param parameters 事件参数
*/
function componentEvent(eventName, componentName, target, instance, parameters) {
if (!parameters) {
parameters = {};
}
// @ts-ignore
parameters.inst = instance;
const fullEventName = `${eventName}.mdui.${componentName}`;
// jQuery 事件
// @ts-ignore
if (typeof jQuery !== 'undefined') {
// @ts-ignore
jQuery(target).trigger(fullEventName, parameters);
}
const $target = $(target);
// mdui.jq 事件
$target.trigger(fullEventName, parameters);
const eventParams = {
bubbles: true,
cancelable: true,
detail: parameters,
};
const eventObject = new CustomEvent(fullEventName, eventParams);
// @ts-ignore
eventObject._detail = parameters;
$target[0].dispatchEvent(eventObject);
}
const container = {};
function queue(name, func) {
if (isUndefined(container[name])) {
container[name] = [];
}
if (isUndefined(func)) {
return container[name];
}
container[name].push(func);
}
/**
* 从队列中移除第一个函数,并执行该函数
* @param name 队列满
*/
function dequeue(name) {
if (isUndefined(container[name])) {
return;
}
if (!container[name].length) {
return;
}
const func = container[name].shift();
func();
}
const DEFAULT_OPTIONS = {
history: true,
overlay: true,
modal: false,
closeOnEsc: true,
closeOnCancel: true,
closeOnConfirm: true,
destroyOnClosed: false,
};
/**
* 当前显示的对话框实例
*/
let currentInst = null;
/**
* 队列名
*/
const queueName = '_mdui_dialog';
/**
* 窗口是否已锁定
*/
let isLockScreen = false;
/**
* 遮罩层元素
*/
let $overlay;
class Dialog {
constructor(selector, options = {}) {
/**
* 配置参数
*/
this.options = extend({}, DEFAULT_OPTIONS);
/**
* 当前 dialog 的状态
*/
this.state = 'closed';
/**
* dialog 元素是否是动态添加的
*/
this.append = false;
this.$element = $(selector).first();
// 如果对话框元素没有在当前文档中,则需要添加
if (!contains(document.body, this.$element[0])) {
this.append = true;
$body.append(this.$element);
}
extend(this.options, options);
// 绑定取消按钮事件
this.$element.find('[mdui-dialog-cancel]').each((_, cancel) => {
$(cancel).on('click', () => {
this.triggerEvent('cancel');
if (this.options.closeOnCancel) {
this.close();
}
});
});
// 绑定确认按钮事件
this.$element.find('[mdui-dialog-confirm]').each((_, confirm) => {
$(confirm).on('click', () => {
this.triggerEvent('confirm');
if (this.options.closeOnConfirm) {
this.close();
}
});
});
// 绑定关闭按钮事件
this.$element.find('[mdui-dialog-close]').each((_, close) => {
$(close).on('click', () => this.close());
});
}
/**
* 触发组件事件
* @param name
*/
triggerEvent(name) {
componentEvent(name, 'dialog', this.$element, this);
}
/**
* 窗口宽度变化,或对话框内容变化时,调整对话框位置和对话框内的滚动条
*/
readjust() {
if (!currentInst) {
return;
}
const $element = currentInst.$element;
const $title = $element.children('.mdui-dialog-title');
const $content = $element.children('.mdui-dialog-content');
const $actions = $element.children('.mdui-dialog-actions');
// 调整 dialog 的 top 和 height 值
$element.height('');
$content.height('');
const elementHeight = $element.height();
$element.css({
top: `${($window.height() - elementHeight) / 2}px`,
height: `${elementHeight}px`,
});
// 调整 mdui-dialog-content 的高度
$content.innerHeight(elementHeight -
($title.innerHeight() || 0) -
($actions.innerHeight() || 0));
}
/**
* hashchange 事件触发时关闭对话框
*/
hashchangeEvent() {
if (window.location.hash.substring(1).indexOf('mdui-dialog') < 0) {
currentInst.close(true);
}
}
/**
* 点击遮罩层关闭对话框
* @param event
*/
overlayClick(event) {
if ($(event.target).hasClass('mdui-overlay') &&
currentInst) {
currentInst.close();
}
}
/**
* 动画结束回调
*/
transitionEnd() {
if (this.$element.hasClass('mdui-dialog-open')) {
this.state = 'opened';
this.triggerEvent('opened');
}
else {
this.state = 'closed';
this.triggerEvent('closed');
this.$element.hide();
// 所有对话框都关闭,且当前没有打开的对话框时,解锁屏幕
if (!queue(queueName).length && !currentInst && isLockScreen) {
$.unlockScreen();
isLockScreen = false;
}
$window.off('resize', $.throttle(this.readjust, 100));
if (this.options.destroyOnClosed) {
this.destroy();
}
}
}
/**
* 打开指定对话框
*/
doOpen() {
currentInst = this;
if (!isLockScreen) {
$.lockScreen();
isLockScreen = true;
}
this.$element.show();
this.readjust();
$window.on('resize', $.throttle(this.readjust, 100));
// 打开消息框
this.state = 'opening';
this.triggerEvent('open');
this.$element
.addClass('mdui-dialog-open')
.transitionEnd(() => this.transitionEnd());
// 不存在遮罩层元素时,添加遮罩层
if (!$overlay) {
$overlay = $.showOverlay(5100);
}
// 点击遮罩层时是否关闭对话框
if (this.options.modal) {
$overlay.off('click', this.overlayClick);
}
else {
$overlay.on('click', this.overlayClick);
}
// 是否显示遮罩层,不显示时,把遮罩层背景透明
$overlay.css('opacity', this.options.overlay ? '' : 0);
if (this.options.history) {
// 如果 hash 中原来就有 mdui-dialog,先删除,避免后退历史纪录后仍然有 mdui-dialog 导致无法关闭
// 包括 mdui-dialog 和 &mdui-dialog 和 ?mdui-dialog
let hash = window.location.hash.substring(1);
if (hash.indexOf('mdui-dialog') > -1) {
hash = hash.replace(/[&?]?mdui-dialog/g, '');
}
// 后退按钮关闭对话框
if (hash) {
window.location.hash = `${hash}${hash.indexOf('?') > -1 ? '&' : '?'}mdui-dialog`;
}
else {
window.location.hash = 'mdui-dialog';
}
$window.on('hashchange', this.hashchangeEvent);
}
}
/**
* 当前对话框是否为打开状态
*/
isOpen() {
return this.state === 'opening' || this.state === 'opened';
}
/**
* 打开对话框
*/
open() {
if (this.isOpen()) {
return;
}
// 如果当前有正在打开或已经打开的对话框,或队列不为空,则先加入队列,等旧对话框开始关闭时再打开
if ((currentInst &&
(currentInst.state === 'opening' || currentInst.state === 'opened')) ||
queue(queueName).length) {
queue(queueName, () => this.doOpen());
return;
}
this.doOpen();
}
/**
* 关闭对话框
*/
close(historyBack = false) {
// historyBack 是否需要后退历史纪录,默认为 `false`。该参数仅内部使用
// 为 `false` 时是通过 js 关闭,需要后退一个历史记录
// 为 `true` 时是通过后退按钮关闭,不需要后退历史记录
// setTimeout 的作用是:
// 当同时关闭一个对话框