diff --git a/packages/editor-framework/.eslintignore b/packages/editor-framework/.eslintignore new file mode 100644 index 000000000..f6ee039b9 --- /dev/null +++ b/packages/editor-framework/.eslintignore @@ -0,0 +1,6 @@ +# 忽略目录 +build/ +node_modules/ +**/*-min.js +**/*.min.js +coverage/ diff --git a/packages/editor-framework/.eslintrc.js b/packages/editor-framework/.eslintrc.js new file mode 100644 index 000000000..ebda54735 --- /dev/null +++ b/packages/editor-framework/.eslintrc.js @@ -0,0 +1,5 @@ +const { eslint, deepmerge } = require('@ice/spec'); + +module.exports = deepmerge(eslint, { + rules: {}, +}); diff --git a/packages/editor-framework/.gitignore b/packages/editor-framework/.gitignore new file mode 100644 index 000000000..84d2d58ff --- /dev/null +++ b/packages/editor-framework/.gitignore @@ -0,0 +1,22 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +CHANGELOG.md diff --git a/packages/editor-framework/.prettierrc b/packages/editor-framework/.prettierrc new file mode 100644 index 000000000..8748c5ed3 --- /dev/null +++ b/packages/editor-framework/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/packages/editor-framework/README copy.md b/packages/editor-framework/README copy.md new file mode 100644 index 000000000..6e9e79f5b --- /dev/null +++ b/packages/editor-framework/README copy.md @@ -0,0 +1,11 @@ +# demo component + +t-s-demo + +intro component + +## API + +| 参数名 | 说明 | 必填 | 类型 | 默认值 | 备注 | +| ------ | ---- | ---- | ---- | ------ | ---- | +| | | | | | | diff --git a/packages/editor-framework/README.md b/packages/editor-framework/README.md index eb99c606a..8a6fb13f0 100644 --- a/packages/editor-framework/README.md +++ b/packages/editor-framework/README.md @@ -1 +1 @@ -编辑器框架 +## todo diff --git a/packages/editor-framework/build.json b/packages/editor-framework/build.json new file mode 100644 index 000000000..77627cdf9 --- /dev/null +++ b/packages/editor-framework/build.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + "build-plugin-component", + "build-plugin-fusion", + ["build-plugin-moment-locales", { + "locales": ["zh-cn"] + }] + ] +} \ No newline at end of file diff --git a/packages/editor-framework/demo/usage.md b/packages/editor-framework/demo/usage.md new file mode 100644 index 000000000..9f19eae0b --- /dev/null +++ b/packages/editor-framework/demo/usage.md @@ -0,0 +1,24 @@ +--- +title: Simple Usage +order: 1 +--- + +本 Demo 演示一行文字的用法。 + +````jsx +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; + +class App extends Component { + render() { + return ( +
+
+ ); + } +} + +ReactDOM.render(( + +), mountNode); +```` diff --git a/packages/editor-framework/es/context.d.ts b/packages/editor-framework/es/context.d.ts new file mode 100644 index 000000000..7836d2580 --- /dev/null +++ b/packages/editor-framework/es/context.d.ts @@ -0,0 +1,3 @@ +/// +declare const context: import("react").Context<{}>; +export default context; diff --git a/packages/editor-framework/es/context.js b/packages/editor-framework/es/context.js new file mode 100644 index 000000000..450ae72fb --- /dev/null +++ b/packages/editor-framework/es/context.js @@ -0,0 +1,3 @@ +import { createContext } from 'react'; +var context = createContext({}); +export default context; \ No newline at end of file diff --git a/packages/editor-framework/package.json b/packages/editor-framework/package.json new file mode 100644 index 000000000..3d1465113 --- /dev/null +++ b/packages/editor-framework/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ali/lowcode-engine-editor", + "version": "0.0.1", + "description": "alibaba lowcode editor core", + "files": [ + "demo/", + "es/", + "lib/", + "build/" + ], + "main": "lib/index.js", + "module": "es/index.js", + "stylePath": "style.js", + "scripts": { + "start": "build-scripts start", + "build": "build-scripts build", + "prepublishOnly": "npm run prettier && npm run build", + "lint": "eslint --cache --ext .js,.jsx ./", + "prettier": "prettier --write \"./src/**/*.{ts,tsx,js,jsx,ejs,less,css,scss,json}\" " + }, + "keywords": [ + "lowcode", + "editor" + ], + "author": "xiayang.xy", + "dependencies": { + "debug": "^4.1.1", + "events": "^3.1.0", + "intl-messageformat": "^7.8.4", + "lodash": "^4.17.15", + "prop-types": "^15.5.8", + "store": "^2.0.12" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.3", + "@alifd/next": "1.x", + "@ice/spec": "^0.1.1", + "@types/lodash": "^4.14.149", + "@types/react": "^16.9.13", + "@types/react-dom": "^16.9.4", + "build-plugin-component": "^0.2.7-1", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "prettier": "^1.19.1", + "react": "^16.8.0", + "react-dom": "^16.8.0" + }, + "peerDependencies": { + "react": "^16.8.0", + "@alifd/next": "1.x" + }, + "license": "MIT", + "homepage": "https://unpkg.com/editor-framework@0.0.1/build/index.html" +} diff --git a/packages/editor-framework/src/context.ts b/packages/editor-framework/src/context.ts new file mode 100644 index 000000000..78d3ce177 --- /dev/null +++ b/packages/editor-framework/src/context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; +const context = createContext({}); +export default context; diff --git a/packages/editor-framework/src/definitions.ts b/packages/editor-framework/src/definitions.ts new file mode 100644 index 000000000..87dd5dca2 --- /dev/null +++ b/packages/editor-framework/src/definitions.ts @@ -0,0 +1,48 @@ + +export interface EditorConfig { + +}; + +export interface NpmConfig { + version: string, + package: string, + main?: string, + exportName?: string, + subName?: string, + destructuring?: boolean +}; + +export interface SkeletonConfig { + config: NpmConfig, + props?: object, + handler?: (EditorConfig) => EditorConfig +}; + +export interface FusionTheme { + package: string, + version: string +}; + +export interface ThemeConfig { + fusion?: FusionTheme +} + +export interface PluginsConfig { + [key]: Array +}; + +export interface PluginConfig { + pluginKey: string, + type: string, + props: object, + config: NpmConfig, + pluginProps: object +}; + +export type HooksConfig = Array; + +export interface HookConfig { + +}; + + diff --git a/packages/editor-framework/src/editor.ts b/packages/editor-framework/src/editor.ts new file mode 100644 index 000000000..b3328df2d --- /dev/null +++ b/packages/editor-framework/src/editor.ts @@ -0,0 +1,186 @@ +import EventEmitter from 'events'; +import Debug from 'debug'; +import store from 'store'; + +import { + unRegistShortCuts, + registShortCuts, + transformToPromise, + generateI18n +} from './utils'; + +// 根据url参数设置debug选项 +const res = /_?debug=(.*?)(&|$)/.exec(location.search); +if (res && res[1]) { + window.__isDebug = true; + store.storage.write('debug', res[1] === 'true' ? '*' : res[1]); +} else { + window.__isDebug = false; + store.remove('debug'); +} + +//重要,用于矫正画布执行new Function的window对象上下文 +window.__newFunc = funContext => { + return new Function(funContext); +}; + +//关闭浏览器前提醒,只有产生过交互才会生效 +window.onbeforeunload = function(e) { + e = e || window.event; + // 本地调试不生效 + if (location.href.indexOf('localhost') > 0) return; + var msg = '您确定要离开此页面吗?'; + e.cancelBubble = true; + e.returnValue = msg; + if (e.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + return msg; +}; + + +let instance = null; +const debug = Debug('editor'); +EventEmitter.defaultMaxListeners = 100; + +export interface editor { + +}; + +export default class Editor extends EventEmitter { + static getInstance = () => { + if (!instance) { + instance = new Editor(); + } + return instance; + }; + + constructor(config) { + super(); + instance = this; + Object.assign(this, config); + this.init(); + } + + init() { + const { + hooks, + shortCuts, + lifeCycles + } = this.config || {}; + this.destroy(); + this.locale = store.get('lowcode-editor-locale') || 'zh-CN'; + this.messages = this.messagesSet[this.locale]; + this.i18n = generateI18n(this.locale, this.messages); + this.pluginStatus = this.initPluginStatus(); + this.initHooks(hooks, appHelper); + + appHelper.emit('editor.beforeInit'); + const init = lifeCycles && lifeCycles.init || () => {}; + // 用户可以通过设置extensions.init自定义初始化流程; + transformToPromise(init(this)) + .then(() => { + // 注册快捷键 + registShortCuts(shortCuts, this); + this.emit('editor.afterInit'); + }) + .catch(err => { + console.warn(err); + }); + } + + destroy() { + try { + const { + hooks = [], + shortCuts = [], + lifeCycles = {} + } = this.config; + unRegistShortCuts(shortCuts); + this.destroyHooks(hooks); + lifeCycles.destroy && lifeCycles.destroy(); + } catch (err) { + console.warn(err); + return; + } + } + + get(key:string):any { + return this[key]; + } + + set(key:string|object, val:any):void { + if (typeof key === 'string') { + if (['init', 'destroy', 'get', 'set', 'batchOn', 'batchOff', 'batchOnce'].includes(key)) { + console.warning('init, destroy, get, set, batchOn, batchOff, batchOnce is private attribute'); + return; + } + this[key] = val; + } else if (typeof key === 'object') { + Object.keys(key).forEach(item => { + this[item] = key[item]; + }); + } + } + + batchOn(events:Array, lisenter:function):void { + if (!Array.isArray(events)) return; + events.forEach(event => this.on(event, lisenter)); + } + + batchOnce(events:Array, lisenter:function):void { + if (!Array.isArray(events)) return; + events.forEach(event => this.once(event, lisenter)); + } + + batchOff(events:Array, lisenter:function):void { + if (!Array.isArray(events)) return; + events.forEach(event => this.off(event, lisenter)); + } + + //销毁hooks中的消息监听 + private destroyHooks(hooks = []) { + hooks.forEach((item, idx) => { + if (typeof this.__hooksFuncs[idx] === 'function') { + this.appHelper.off(item.message, this.__hooksFuncs[idx]); + } + }); + delete this.__hooksFuncs; + }; + + //初始化hooks中的消息监听 + private initHooks(hooks = []) { + this.__hooksFuncs = hooks.map(item => { + const func = (...args) => { + item.handler(this, ...args); + }; + this[item.type](item.message, func); + return func; + }); + }; + + + private initPluginStatus () { + const {plugins = {}} = this.config; + const pluginAreas = Object.keys(plugins); + const res = {}; + pluginAreas.forEach(area => { + (plugins[area] || []).forEach(plugin => { + if (plugin.type === 'Divider') return; + const { visible, disabled, dotted } = plugin.props || {}; + res[plugin.pluginKey] = { + visible: typeof visible === 'boolean' ? visible : true, + disabled: typeof disabled === 'boolean' ? disabled : false, + dotted: typeof dotted === 'boolean' ? dotted : false + }; + const pluginClass = this.props.components[skeletonUtils.generateAddonCompName(addon.addonKey)]; + // 判断如果编辑器插件有init静态方法,则在此执行init方法 + if (pluginClass && pluginClass.init) { + pluginClass.init(this); + } + }); + }); + return res; + }; +} diff --git a/packages/editor-framework/src/index.ts b/packages/editor-framework/src/index.ts new file mode 100644 index 000000000..aac18d138 --- /dev/null +++ b/packages/editor-framework/src/index.ts @@ -0,0 +1,4 @@ +import Editor from './editor'; + + +export default Editor; \ No newline at end of file diff --git a/packages/editor-framework/src/plugin.ts b/packages/editor-framework/src/plugin.ts new file mode 100644 index 000000000..33d81cc48 --- /dev/null +++ b/packages/editor-framework/src/plugin.ts @@ -0,0 +1,129 @@ +import { PureComponent } from 'react'; + +import EditorContext from './context'; +import { isEmpty, generateI18n, goldlog } from './utils'; + +export interface pluginProps { + config: object, + editor: object, + locale: string, + messages: object +} + +export default function plugin(Comp) { + + class Plugin extends PureComponent { + static displayName = 'lowcode-editor-plugin'; + static defaultProps = { + config: {} + }; + static contextType = EditorContext; + constructor(props, context) { + super(props, context); + if (isEmpty(props.config) || !props.config.pluginKey) { + console.warn('lowcode editor plugin has wrong config'); + return; + } + + const { locale, messages, editor } = props; + // 注册插件 + this.editor = editor; + this.i18n = generateI18n(locale, messages); + this.pluginKey = props.config.pluginKey; + editor.plugins = editor.plugins || {}; + editor.plugins[this.pluginKey] = this; + } + + componentWillUnmount() { + // 销毁插件 + if (this.editor && this.editor.plugins) { + delete this.editor.plugins[this.pluginKey]; + } + } + + render() { + const { + config + } = this.props; + return + } + } + + return Plugin; +} + + + +export class Plugin extends PureComponent { + static displayName = 'lowcode-editor-plugin'; + static defaultProps = { + config: {} + }; + static contextType = EditorContext; + constructor(props, context) { + super(props, context); + if (isEmpty(props.config) || !props.config.addonKey) { + console.warn('luna addon has wrong config'); + return; + } + + + const { locale, messages, editor } = props; + // 注册插件 + this.editor = editor; + this.i18n = generateI18n(locale, messages); + this.pluginKey = props.config.pluginKey; + editor.plugins = editor.plugins || {}; + editor.plugins[this.pluginKey] = this; + } + + async componentWillUnmount() { + // 销毁插件 + if (this.editor && this.editor.plugins) { + delete this.editor.plugins[this.pluginKey]; + } + } + + open = () => { + return true; + }; + + close = () => { + return true; + }; + + goldlog = (goKey:string, params:any) => { + const { pluginKey, config = {} } = this.props.config || {}; + goldlog( + goKey, + { + pluginKey, + package: config.package, + version: config.version, + ...this.editor.logParams, + ...params + }, + 'addon' + ); + }; + + get utils() { + return this.editor.utils; + } + + get constants() { + return this.editor.constants; + } + + get history() { + return this.editor.history; + } + + get location() { + return this.editor.location; + } + + render() { + return null; + } +} diff --git a/packages/editor-framework/src/utils.ts b/packages/editor-framework/src/utils.ts new file mode 100644 index 000000000..d1c5eaf2b --- /dev/null +++ b/packages/editor-framework/src/utils.ts @@ -0,0 +1,242 @@ + +import IntlMessageFormat from 'intl-messageformat'; +import _isEmpty from 'lodash/isEmpty'; + +export const isEmpty = _isEmpty; + +/** + * 用于构造国际化字符串处理函数 + * @param {*} locale 国际化标识,例如 zh-CN、en-US + * @param {*} messages 国际化语言包 + */ +export function generateI18n(locale = 'zh-CN', messages = {}) { + return (key, values = {}) => { + if (!messages || !messages[key]) return ''; + const formater = new IntlMessageFormat(messages[key], locale); + return formater.format(values); + }; +} + +/** + * 序列化参数 + * @param {*} obj 参数 + */ +export function serializeParams(obj:object):string { + if (typeof obj !== 'object') return ''; + + const res:Array = []; + Object.entries(obj).forEach(([key, val]) => { + if (val === null || val === undefined || val === '') return; + if (typeof val === 'object') { + res.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(val))}`); + } else { + res.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`); + } + }); + return res.join('&'); +} + +/** + * 黄金令箭埋点 + * @param {String} gmKey 为黄金令箭业务类型 + * @param {Object} params 参数 + * @param {String} logKey 属性串 + */ +export function goldlog(gmKey, params = {}, logKey = 'other') { + const sendIDEMessage = window.sendIDEMessage || window.parent.sendIDEMessage; + const goKey = serializeParams({ + sdkVersion: pkg.version, + env: getEnv(), + ...params + }); + if (sendIDEMessage) { + sendIDEMessage({ + action: 'goldlog', + data: { + logKey: `/iceluna.core.${logKey}`, + gmKey, + goKey + } + }); + } + window.goldlog && window.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST'); +} + +/** + * 获取当前编辑器环境 + */ +export function getEnv() { + const userAgent = navigator.userAgent; + const isVscode = /Electron\//.test(userAgent); + if (isVscode) return ENV.VSCODE; + const isTheia = window.is_theia === true; + if (isTheia) return ENV.WEBIDE; + return ENV.WEB; +} + +// 注册快捷键 +export function registShortCuts(config, editor) { + const keyboardFilter = (keymaster.filter = event => { + let eTarget = event.target || event.srcElement; + let tagName = eTarget.tagName; + let isInput = !!(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); + let isContenteditable = !!eTarget.getAttribute('contenteditable'); + if (isInput || isContenteditable) { + if (event.metaKey === true && [70, 83].includes(event.keyCode)) event.preventDefault(); //禁止触发chrome原生的页面保存或查找 + return false; + } else { + return true; + } + }); + + const ideMessage = appHelper.utils && appHelper.utils.ideMessage; + + //复制 + if (!document.copyListener) { + document.copyListener = e => { + if (!keyboardFilter(e) || appHelper.isCopying) return; + const schema = appHelper.schemaHelper && appHelper.schemaHelper.schemaMap[appHelper.activeKey]; + if (!schema || !isSchema(schema)) return; + appHelper.isCopying = true; + const schemaStr = serialize(transformSchemaToPure(schema), { + unsafe: true + }); + setClipboardData(schemaStr) + .then(() => { + ideMessage && ideMessage('success', '当前内容已复制到剪贴板,请使用快捷键Command+v进行粘贴'); + appHelper.emit('schema.copy', schemaStr, schema); + appHelper.isCopying = false; + }) + .catch(errMsg => { + ideMessage && ideMessage('error', errMsg); + appHelper.isCopying = false; + }); + }; + document.addEventListener('copy', document.copyListener); + if (window.parent.vscode) { + keymaster('command+c', document.copyListener); + } + } + + //粘贴 + if (!document.pasteListener) { + const doPaste = (e, text) => { + if (!keyboardFilter(e) || appHelper.isPasting) return; + const schemaHelper = appHelper.schemaHelper; + let targetKey = appHelper.activeKey; + let direction = 'after'; + const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey; + if (!targetKey || topKey === targetKey) { + const schemaHelper = appHelper.schemaHelper; + const topKey = schemaHelper.schema && schemaHelper.schema.__ctx && schemaHelper.schema.__ctx.lunaKey; + if (!topKey) return; + targetKey = topKey; + direction = 'in'; + } + appHelper.isPasting = true; + const schema = parseObj(text); + if (!isSchema(schema)) { + appHelper.emit('illegalSchema.paste', text); + // ideMessage && ideMessage('error', '当前内容不是模型结构,不能粘贴进来!'); + console.warn('paste schema illegal'); + appHelper.isPasting = false; + return; + } + appHelper.emit('material.add', { + schema, + targetKey, + direction + }); + appHelper.isPasting = false; + appHelper.emit('schema.paste', schema); + }; + document.pasteListener = e => { + const clipboardData = e.clipboardData || window.clipboardData; + const text = clipboardData && clipboardData.getData('text'); + doPaste(e, text); + }; + document.addEventListener('paste', document.pasteListener); + if (window.parent.vscode) { + keymaster('command+v', e => { + const sendIDEMessage = window.parent.sendIDEMessage; + sendIDEMessage && + sendIDEMessage({ + action: 'readClipboard' + }) + .then(text => { + doPaste(e, text); + }) + .catch(err => { + console.warn(err); + }); + }); + } + } + + (config || []).forEach(item => { + keymaster(item.keyboard, ev => { + ev.preventDefault(); + item.handler(ev, appHelper, keymaster); + }); + }); +} + +// 取消注册快捷 +export function unRegistShortCuts(config) { + (config || []).forEach(item => { + keymaster.unbind(item.keyboard); + }); + if (window.parent.vscode) { + keymaster.unbind('command+c'); + keymaster.unbind('command+v'); + } + if (document.copyListener) { + document.removeEventListener('copy', document.copyListener); + delete document.copyListener; + } + if (document.pasteListener) { + document.removeEventListener('paste', document.pasteListener); + delete document.pasteListener; + } +} + +// 将函数返回结果转成promise形式,如果函数有返回值则根据返回值的bool类型判断是reject还是resolve,若函数无返回值默认执行resolve +export function transformToPromise(input) { + if (input instanceof Promise) return input; + return new Promise((resolve, reject) => { + if (input || input === undefined) { + resolve(); + } else { + reject(); + } + }); +} + +export function comboEditorConfig(defaultConfig, customConfig) { + const { ideConfig = {}, utils = {} } = this.props; + const comboShortCuts = () => { + const defaultShortCuts = defaultIdeConfig.shortCuts; + const shortCuts = ideConfig.shortCuts || []; + const configMap = skeletonUtils.transformArrayToMap(defaultShortCuts, 'keyboard'); + (shortCuts || []).forEach(item => { + configMap[item.keyboard] = item; + }); + return Object.keys(configMap).map(key => configMap[key]); + }; + return { + ...ideConfig, + utils: { + ...skeletonUtils, + ...utils + }, + constants: { + ...defaultIdeConfig.constants, + ...ideConfig.constants + }, + extensions: { + ...defaultIdeConfig.extensions, + ...ideConfig.extensions + }, + shortCuts: comboShortCuts() + }; +} \ No newline at end of file diff --git a/packages/editor-framework/tsconfig.json b/packages/editor-framework/tsconfig.json new file mode 100644 index 000000000..a511d68ba --- /dev/null +++ b/packages/editor-framework/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "buildOnSave": false, + "compilerOptions": { + "outDir": "build", + "module": "esnext", + "target": "es6", + "jsx": "react", + "moduleResolution": "node", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "skipLibCheck": true + }, + "include": ["src/*.ts", "src/*.tsx"], + "exclude": ["node_modules", "build", "public"] +} diff --git a/packages/editor-skeleton/.editorconfig b/packages/editor-skeleton/.editorconfig new file mode 100644 index 000000000..5760be583 --- /dev/null +++ b/packages/editor-skeleton/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/packages/editor-skeleton/.eslintignore b/packages/editor-skeleton/.eslintignore new file mode 100644 index 000000000..3b437e614 --- /dev/null +++ b/packages/editor-skeleton/.eslintignore @@ -0,0 +1,11 @@ +# 忽略目录 +build/ +tests/ +demo/ + +# node 覆盖率文件 +coverage/ + +# 忽略文件 +**/*-min.js +**/*.min.js diff --git a/packages/editor-skeleton/.eslintrc.js b/packages/editor-skeleton/.eslintrc.js new file mode 100644 index 000000000..18ae6baa7 --- /dev/null +++ b/packages/editor-skeleton/.eslintrc.js @@ -0,0 +1,7 @@ +const { eslint, deepmerge } = require('@ice/spec'); + +module.exports = deepmerge(eslint, { + rules: { + "global-require": 0, + }, +}); diff --git a/packages/editor-skeleton/.gitignore b/packages/editor-skeleton/.gitignore new file mode 100644 index 000000000..c590da5b0 --- /dev/null +++ b/packages/editor-skeleton/.gitignore @@ -0,0 +1,20 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/build +/dist + +# misc +.idea/ +.happypack +.DS_Store + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# ignore d.ts auto generated by css-modules-typescript-loader +*.module.scss.d.ts \ No newline at end of file diff --git a/packages/editor-skeleton/.stylelintignore b/packages/editor-skeleton/.stylelintignore new file mode 100644 index 000000000..82af6f60d --- /dev/null +++ b/packages/editor-skeleton/.stylelintignore @@ -0,0 +1,7 @@ +# 忽略目录 +build/ +tests/ +demo/ + +# node 覆盖率文件 +coverage/ diff --git a/packages/editor-skeleton/.stylelintrc.js b/packages/editor-skeleton/.stylelintrc.js new file mode 100644 index 000000000..eeb605b33 --- /dev/null +++ b/packages/editor-skeleton/.stylelintrc.js @@ -0,0 +1,3 @@ +const { stylelint } = require('@ice/spec'); + +module.exports = stylelint; diff --git a/packages/editor-skeleton/README.md b/packages/editor-skeleton/README.md new file mode 100644 index 000000000..8a6fb13f0 --- /dev/null +++ b/packages/editor-skeleton/README.md @@ -0,0 +1 @@ +## todo diff --git a/packages/editor-skeleton/abc.json b/packages/editor-skeleton/abc.json new file mode 100644 index 000000000..dce1f92ed --- /dev/null +++ b/packages/editor-skeleton/abc.json @@ -0,0 +1,4 @@ +{ + "type": "ice-scripts", + "builder": "@ali/builder-ice-scripts" +} diff --git a/packages/editor-skeleton/build.json b/packages/editor-skeleton/build.json new file mode 100644 index 000000000..77627cdf9 --- /dev/null +++ b/packages/editor-skeleton/build.json @@ -0,0 +1,9 @@ +{ + "plugins": [ + "build-plugin-component", + "build-plugin-fusion", + ["build-plugin-moment-locales", { + "locales": ["zh-cn"] + }] + ] +} \ No newline at end of file diff --git a/packages/editor-skeleton/demo/usage.md b/packages/editor-skeleton/demo/usage.md new file mode 100644 index 000000000..9f19eae0b --- /dev/null +++ b/packages/editor-skeleton/demo/usage.md @@ -0,0 +1,24 @@ +--- +title: Simple Usage +order: 1 +--- + +本 Demo 演示一行文字的用法。 + +````jsx +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; + +class App extends Component { + render() { + return ( +
+
+ ); + } +} + +ReactDOM.render(( + +), mountNode); +```` diff --git a/packages/editor-skeleton/es/components/LeftIcon/index.js b/packages/editor-skeleton/es/components/LeftIcon/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor-skeleton/es/components/LeftIcon/index.scss b/packages/editor-skeleton/es/components/LeftIcon/index.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor-skeleton/es/components/LeftPlugin/index.d.ts b/packages/editor-skeleton/es/components/LeftPlugin/index.d.ts new file mode 100644 index 000000000..312fc40aa --- /dev/null +++ b/packages/editor-skeleton/es/components/LeftPlugin/index.d.ts @@ -0,0 +1,30 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class LeftAddon extends PureComponent { + static displayName: string; + static propTypes: { + active: any; + config: any; + disabled: any; + dotted: any; + locked: any; + onClick: any; + }; + static defaultProps: { + active: boolean; + config: {}; + disabled: boolean; + dotted: boolean; + locked: boolean; + onClick: () => void; + }; + static contextType: any; + constructor(props: any, context: any); + componentDidMount(): void; + componentWillUnmount(): void; + handleClose: () => void; + handleOpen: () => void; + handleShow: () => void; + renderIcon: (clickCallback: any) => JSX.Element; + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/components/LeftPlugin/index.js b/packages/editor-skeleton/es/components/LeftPlugin/index.js new file mode 100644 index 000000000..517104928 --- /dev/null +++ b/packages/editor-skeleton/es/components/LeftPlugin/index.js @@ -0,0 +1,259 @@ +import _extends from "@babel/runtime/helpers/extends"; +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import AppContext from '@ali/iceluna-sdk/lib/context/appContext'; +import { Balloon, Dialog, Icon, Badge } from '@alife/next'; +import './index.scss'; + +var LeftAddon = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(LeftAddon, _PureComponent); + + function LeftAddon(_props, context) { + var _this; + + _this = _PureComponent.call(this, _props, context) || this; + + _this.handleClose = function () { + var addonKey = _this.props.config && _this.props.config.addonKey; + var currentAddon = _this.appHelper.addons && _this.appHelper.addons[addonKey]; + + if (currentAddon) { + _this.utils.transformToPromise(currentAddon.close()).then(function () { + _this.setState({ + dialogVisible: false + }); + }); + } + }; + + _this.handleOpen = function () { + // todo 对话框类型的插件初始时拿不到插件实例 + _this.setState({ + dialogVisible: true + }); + }; + + _this.handleShow = function () { + var _this$props = _this.props, + disabled = _this$props.disabled, + config = _this$props.config, + onClick = _this$props.onClick; + var addonKey = config && config.addonKey; + if (disabled || !addonKey) return; //考虑到弹窗情况,延时发送消息 + + setTimeout(function () { + return _this.appHelper.emit(addonKey + ".addon.activate"); + }, 0); + + _this.handleOpen(); + + onClick && onClick(); + }; + + _this.renderIcon = function (clickCallback) { + var _this$props2 = _this.props, + active = _this$props2.active, + disabled = _this$props2.disabled, + dotted = _this$props2.dotted, + locked = _this$props2.locked, + _onClick = _this$props2.onClick, + config = _this$props2.config; + + var _ref = config || {}, + addonKey = _ref.addonKey, + props = _ref.props; + + var _ref2 = props || {}, + icon = _ref2.icon, + title = _ref2.title; + + return React.createElement("div", { + className: classNames('luna-left-addon', addonKey, { + active: active, + disabled: disabled, + locked: locked + }), + "data-tooltip": title, + onClick: function onClick() { + if (disabled) return; //考虑到弹窗情况,延时发送消息 + + clickCallback && clickCallback(); + _onClick && _onClick(); + } + }, dotted ? React.createElement(Badge, { + dot: true + }, React.createElement(Icon, { + type: icon, + size: "small" + })) : React.createElement(Icon, { + type: icon, + size: "small" + })); + }; + + _this.state = { + dialogVisible: false + }; + _this.appHelper = context.appHelper; + _this.utils = _this.appHelper.utils; + _this.constants = _this.appHelper.constants; + return _this; + } + + var _proto = LeftAddon.prototype; + + _proto.componentDidMount = function componentDidMount() { + var config = this.props.config; + var addonKey = config && config.addonKey; + var appHelper = this.appHelper; + + if (appHelper && addonKey) { + appHelper.on(addonKey + ".dialog.show", this.handleShow); + appHelper.on(addonKey + ".dialog.close", this.handleClose); + } + }; + + _proto.componentWillUnmount = function componentWillUnmount() { + var config = this.props.config; + var appHelper = this.appHelper; + var addonKey = config && config.addonKey; + + if (appHelper && addonKey) { + appHelper.off(addonKey + ".dialog.show", this.handleShow); + appHelper.off(addonKey + ".dialog.close", this.handleClose); + } + }; + + _proto.render = function render() { + var _this2 = this; + + var _this$props3 = this.props, + dotted = _this$props3.dotted, + locked = _this$props3.locked, + active = _this$props3.active, + disabled = _this$props3.disabled, + config = _this$props3.config; + + var _ref3 = config || {}, + addonKey = _ref3.addonKey, + props = _ref3.props, + type = _ref3.type, + addonProps = _ref3.addonProps; + + var _ref4 = props || {}, + _onClick2 = _ref4.onClick, + title = _ref4.title; + + var dialogVisible = this.state.dialogVisible; + var _this$context = this.context, + appHelper = _this$context.appHelper, + components = _this$context.components; + if (!addonKey || !type || !props) return null; + var componentName = appHelper.utils.generateAddonCompName(addonKey); + var localeProps = {}; + var locale = appHelper.locale, + messages = appHelper.messages; + + if (locale) { + localeProps.locale = locale; + } + + if (messages && messages[componentName]) { + localeProps.messages = messages[componentName]; + } + + var AddonComp = components && components[componentName]; + var node = AddonComp && React.createElement(AddonComp, _extends({ + active: active, + locked: locked, + disabled: disabled, + config: config, + onClick: function onClick() { + _onClick2 && _onClick2.call(null, appHelper); + } + }, localeProps, addonProps || {})) || null; + + switch (type) { + case 'LinkIcon': + return React.createElement("a", props.linkProps || {}, this.renderIcon(function () { + _onClick2 && _onClick2.call(null, appHelper); + })); + + case 'Icon': + return this.renderIcon(function () { + _onClick2 && _onClick2.call(null, appHelper); + }); + + case 'DialogIcon': + return React.createElement(Fragment, null, this.renderIcon(function () { + _onClick2 && _onClick2.call(null, appHelper); + + _this2.handleOpen(); + }), React.createElement(Dialog, _extends({ + onOk: function onOk() { + appHelper.emit(addonKey + ".dialog.onOk"); + + _this2.handleClose(); + }, + onCancel: this.handleClose, + onClose: this.handleClose, + title: title + }, props.dialogProps || {}, { + visible: dialogVisible + }), node)); + + case 'BalloonIcon': + return React.createElement(Balloon, _extends({ + trigger: this.renderIcon(function () { + _onClick2 && _onClick2.call(null, appHelper); + }), + align: "r", + triggerType: ['click', 'hover'] + }, props.balloonProps || {}), node); + + case 'PanelIcon': + return this.renderIcon(function () { + _onClick2 && _onClick2.call(null, appHelper); + + _this2.handleOpen(); + }); + + case 'Custom': + return dotted ? React.createElement(Badge, { + dot: true + }, node) : node; + + default: + return null; + } + }; + + return LeftAddon; +}(PureComponent); + +LeftAddon.displayName = 'LunaLeftAddon'; +LeftAddon.propTypes = { + active: PropTypes.bool, + config: PropTypes.shape({ + addonKey: PropTypes.string, + addonProps: PropTypes.object, + props: PropTypes.object, + type: PropTypes.oneOf(['DialogIcon', 'BalloonIcon', 'PanelIcon', 'LinkIcon', 'Icon', 'Custom']) + }), + disabled: PropTypes.bool, + dotted: PropTypes.bool, + locked: PropTypes.bool, + onClick: PropTypes.func +}; +LeftAddon.defaultProps = { + active: false, + config: {}, + disabled: false, + dotted: false, + locked: false, + onClick: function onClick() {} +}; +LeftAddon.contextType = AppContext; +export { LeftAddon as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/components/LeftPlugin/index.scss b/packages/editor-skeleton/es/components/LeftPlugin/index.scss new file mode 100644 index 000000000..9c6922129 --- /dev/null +++ b/packages/editor-skeleton/es/components/LeftPlugin/index.scss @@ -0,0 +1,59 @@ +.luna-left-addon { + font-size: 16px; + text-align: center; + line-height: 36px; + height: 36px; + position: relative; + cursor: pointer; + transition: all 0.3s ease; + color: #777; + &.collapse { + height: 40px; + color: #8c8c8c; + border-bottom: 1px solid #bfbfbf; + } + &.locked { + color: red !important; + } + &.active { + color: #fff !important; + background-color: $color-brand1-9 !important; + &.disabled { + color: #fff; + background-color: $color-fill1-7; + } + } + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + &:hover { + background-color: $color-brand1-1; + color: $color-brand1-6; + &:before { + content: attr(data-tooltip); + display: block; + position: absolute; + left: 50px; + top: 5px; + line-height: 18px; + font-size: 12px; + white-space: nowrap; + padding: 6px 8px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: #fff; + z-index: 100; + } + &:after { + content: ''; + display: block; + position: absolute; + left: 40px; + top: 15px; + border: 5px solid transparent; + border-right-color: rgba(0, 0, 0, 0.75); + z-index: 100; + } + } +} diff --git a/packages/editor-skeleton/es/components/Panel/index.js b/packages/editor-skeleton/es/components/Panel/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor-skeleton/es/components/TopIcon/index.d.ts b/packages/editor-skeleton/es/components/TopIcon/index.d.ts new file mode 100644 index 000000000..2bb6854b4 --- /dev/null +++ b/packages/editor-skeleton/es/components/TopIcon/index.d.ts @@ -0,0 +1,30 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class TopIcon extends PureComponent { + static displayName: string; + static propTypes: { + active: any; + className: any; + disabled: any; + icon: any; + id: any; + locked: any; + onClick: any; + showTitle: any; + style: any; + title: any; + }; + static defaultProps: { + active: boolean; + className: string; + disabled: boolean; + icon: string; + id: string; + locked: boolean; + onClick: () => void; + showTitle: boolean; + style: {}; + title: string; + }; + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/components/TopIcon/index.js b/packages/editor-skeleton/es/components/TopIcon/index.js new file mode 100644 index 000000000..bfa50a96b --- /dev/null +++ b/packages/editor-skeleton/es/components/TopIcon/index.js @@ -0,0 +1,76 @@ +import _Button from "@alifd/next/es/button"; +import _Icon from "@alifd/next/es/icon"; +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import './index.scss'; + +var TopIcon = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(TopIcon, _PureComponent); + + function TopIcon() { + return _PureComponent.apply(this, arguments) || this; + } + + var _proto = TopIcon.prototype; + + _proto.render = function render() { + var _this$props = this.props, + active = _this$props.active, + disabled = _this$props.disabled, + icon = _this$props.icon, + locked = _this$props.locked, + title = _this$props.title, + className = _this$props.className, + id = _this$props.id, + style = _this$props.style, + showTitle = _this$props.showTitle, + onClick = _this$props.onClick; + return React.createElement(_Button, { + type: "normal", + size: "large", + text: true, + className: classNames('lowcode-top-btn', className, { + active: active, + disabled: disabled, + locked: locked + }), + id: id, + style: style, + onClick: disabled ? null : onClick + }, React.createElement("div", null, React.createElement(_Icon, { + size: "large", + type: icon + }), showTitle && React.createElement("span", null, title))); + }; + + return TopIcon; +}(PureComponent); + +TopIcon.displayName = 'TopIcon'; +TopIcon.propTypes = { + active: PropTypes.bool, + className: PropTypes.string, + disabled: PropTypes.bool, + icon: PropTypes.string, + id: PropTypes.string, + locked: PropTypes.bool, + onClick: PropTypes.func, + showTitle: PropTypes.bool, + style: PropTypes.object, + title: PropTypes.string +}; +TopIcon.defaultProps = { + active: false, + className: '', + disabled: false, + icon: '', + id: '', + locked: false, + onClick: function onClick() {}, + showTitle: false, + style: {}, + title: '' +}; +export { TopIcon as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/components/TopIcon/index.scss b/packages/editor-skeleton/es/components/TopIcon/index.scss new file mode 100644 index 000000000..1cb3bdfdf --- /dev/null +++ b/packages/editor-skeleton/es/components/TopIcon/index.scss @@ -0,0 +1,32 @@ +.next-btn.next-large.lowcode-top-btn { + width: 44px; + height: 44px; + padding: 0; + margin: 4px -2px; + text-align: center; + border-radius: 8px; + border: 1px solid transparent; + color: #777; + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + &.locked { + color: red !important; + } + i.next-icon { + &:before { + font-size: 17px; + } + margin-right: 0; + line-height: 18px; + } + span { + display: block; + margin: 0px -5px 0; + line-height: 16px; + text-align: center; + font-size: 12px; + transform: scale(0.8); + } +} diff --git a/packages/editor-skeleton/es/components/TopPlugin/index.d.ts b/packages/editor-skeleton/es/components/TopPlugin/index.d.ts new file mode 100644 index 000000000..dc09c377e --- /dev/null +++ b/packages/editor-skeleton/es/components/TopPlugin/index.d.ts @@ -0,0 +1,21 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class TopPlugin extends PureComponent { + static displayName: string; + static defaultProps: { + active: boolean; + config: {}; + disabled: boolean; + dotted: boolean; + locked: boolean; + onClick: () => void; + }; + constructor(props: any, context: any); + componentDidMount(): void; + componentWillUnmount(): void; + handleShow: () => void; + handleClose: () => void; + handleOpen: () => void; + renderIcon: (clickCallback: any) => JSX.Element; + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/components/TopPlugin/index.js b/packages/editor-skeleton/es/components/TopPlugin/index.js new file mode 100644 index 000000000..e881bb6c1 --- /dev/null +++ b/packages/editor-skeleton/es/components/TopPlugin/index.js @@ -0,0 +1,213 @@ +import _Balloon from "@alifd/next/es/balloon"; +import _Dialog from "@alifd/next/es/dialog"; +import _extends from "@babel/runtime/helpers/extends"; +import _Badge from "@alifd/next/es/badge"; +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent, Fragment } from 'react'; +import TopIcon from '../TopIcon'; +import './index.scss'; + +var TopPlugin = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(TopPlugin, _PureComponent); + + function TopPlugin(_props, context) { + var _this; + + _this = _PureComponent.call(this, _props, context) || this; + + _this.handleShow = function () { + var _this$props = _this.props, + disabled = _this$props.disabled, + config = _this$props.config, + onClick = _this$props.onClick; + var addonKey = config && config.addonKey; + if (disabled || !addonKey) return; //考虑到弹窗情况,延时发送消息 + + setTimeout(function () { + return _this.appHelper.emit(addonKey + ".addon.activate"); + }, 0); + + _this.handleOpen(); + + onClick && onClick(); + }; + + _this.handleClose = function () { + var addonKey = _this.props.config && _this.props.config.addonKey; + var currentAddon = _this.appHelper.addons && _this.appHelper.addons[addonKey]; + + if (currentAddon) { + _this.utils.transformToPromise(currentAddon.close()).then(function () { + _this.setState({ + dialogVisible: false + }); + }); + } + }; + + _this.handleOpen = function () { + // todo dialog类型的插件初始时拿不动插件实例 + _this.setState({ + dialogVisible: true + }); + }; + + _this.renderIcon = function (clickCallback) { + var _this$props2 = _this.props, + active = _this$props2.active, + disabled = _this$props2.disabled, + dotted = _this$props2.dotted, + locked = _this$props2.locked, + config = _this$props2.config, + _onClick = _this$props2.onClick; + + var _ref = config || {}, + pluginKey = _ref.pluginKey, + props = _ref.props; + + var _ref2 = props || {}, + icon = _ref2.icon, + title = _ref2.title; + + var node = React.createElement(TopIcon, { + className: "lowcode-top-addon " + pluginKey, + active: active, + disabled: disabled, + locked: locked, + icon: icon, + title: title, + onClick: function onClick() { + if (disabled) return; //考虑到弹窗情况,延时发送消息 + + setTimeout(function () { + return _this.appHelper.emit(pluginKey + ".addon.activate"); + }, 0); + clickCallback && clickCallback(); + _onClick && _onClick(); + } + }); + return dotted ? React.createElement(_Badge, { + dot: true + }, node) : node; + }; + + _this.state = { + dialogVisible: false + }; + return _this; + } + + var _proto = TopPlugin.prototype; + + _proto.componentDidMount = function componentDidMount() { + var config = this.props.config; + var pluginKey = config && config.pluginKey; // const appHelper = this.appHelper; + // if (appHelper && addonKey) { + // appHelper.on(`${addonKey}.dialog.show`, this.handleShow); + // appHelper.on(`${addonKey}.dialog.close`, this.handleClose); + // } + }; + + _proto.componentWillUnmount = function componentWillUnmount() {// const { config } = this.props; + // const addonKey = config && config.addonKey; + // const appHelper = this.appHelper; + // if (appHelper && addonKey) { + // appHelper.off(`${addonKey}.dialog.show`, this.handleShow); + // appHelper.off(`${addonKey}.dialog.close`, this.handleClose); + // } + }; + + _proto.render = function render() { + var _this2 = this; + + var _this$props3 = this.props, + active = _this$props3.active, + dotted = _this$props3.dotted, + locked = _this$props3.locked, + disabled = _this$props3.disabled, + config = _this$props3.config, + editor = _this$props3.editor, + Comp = _this$props3.pluginClass; + + var _ref3 = config || {}, + pluginKey = _ref3.pluginKey, + pluginProps = _ref3.pluginProps, + props = _ref3.props, + type = _ref3.type; + + var _ref4 = props || {}, + _onClick2 = _ref4.onClick, + title = _ref4.title; + + var dialogVisible = this.state.dialogVisible; + if (!pluginKey || !type || !Comp) return null; + var node = React.createElement(Comp, _extends({ + active: active, + locked: locked, + disabled: disabled, + config: config, + onClick: function onClick() { + _onClick2 && _onClick2.call(null, editor); + } + }, pluginProps)); + + switch (type) { + case 'LinkIcon': + return React.createElement("a", props.linkProps, this.renderIcon(function () { + _onClick2 && _onClick2.call(null, editor); + })); + + case 'Icon': + return this.renderIcon(function () { + _onClick2 && _onClick2.call(null, editor); + }); + + case 'DialogIcon': + return React.createElement(Fragment, null, this.renderIcon(function () { + _onClick2 && _onClick2.call(null, editor); + + _this2.handleOpen(); + }), React.createElement(_Dialog, _extends({ + onOk: function onOk() { + editor.emit(pluginKey + ".dialog.onOk"); + + _this2.handleClose(); + }, + onCancel: this.handleClose, + onClose: this.handleClose, + title: title + }, props.dialogProps, { + visible: dialogVisible + }), node)); + + case 'BalloonIcon': + return React.createElement(_Balloon, _extends({ + trigger: this.renderIcon(function () { + _onClick2 && _onClick2.call(null, editor); + }), + triggerType: ['click', 'hover'] + }, props.balloonProps), node); + + case 'Custom': + return dotted ? React.createElement(_Badge, { + dot: true + }, node) : node; + + default: + return null; + } + }; + + return TopPlugin; +}(PureComponent); + +TopPlugin.displayName = 'lowcodeTopPlugin'; +TopPlugin.defaultProps = { + active: false, + config: {}, + disabled: false, + dotted: false, + locked: false, + onClick: function onClick() {} +}; +export { TopPlugin as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/components/TopPlugin/index.scss b/packages/editor-skeleton/es/components/TopPlugin/index.scss new file mode 100644 index 000000000..4bdd7b8d2 --- /dev/null +++ b/packages/editor-skeleton/es/components/TopPlugin/index.scss @@ -0,0 +1,2 @@ +.lowcode-top-addon { +} diff --git a/packages/editor-skeleton/es/config/skeleton.d.ts b/packages/editor-skeleton/es/config/skeleton.d.ts new file mode 100644 index 000000000..67ae8a0ae --- /dev/null +++ b/packages/editor-skeleton/es/config/skeleton.d.ts @@ -0,0 +1,14 @@ +declare const routerConfig: { + path: string; + component: any; + children: ({ + path: string; + component: any; + redirect?: undefined; + } | { + path: string; + redirect: string; + component?: undefined; + })[]; +}[]; +export default routerConfig; diff --git a/packages/editor-skeleton/es/config/skeleton.js b/packages/editor-skeleton/es/config/skeleton.js new file mode 100644 index 000000000..8fb0727ab --- /dev/null +++ b/packages/editor-skeleton/es/config/skeleton.js @@ -0,0 +1,14 @@ +import Dashboard from '@/pages/Dashboard'; +import BasicLayout from '@/layouts/BasicLayout'; +var routerConfig = [{ + path: '/', + component: BasicLayout, + children: [{ + path: '/dashboard', + component: Dashboard + }, { + path: '/', + redirect: '/dashboard' + }] +}]; +export default routerConfig; \ No newline at end of file diff --git a/packages/editor-skeleton/es/config/utils.d.ts b/packages/editor-skeleton/es/config/utils.d.ts new file mode 100644 index 000000000..5e5bda5ab --- /dev/null +++ b/packages/editor-skeleton/es/config/utils.d.ts @@ -0,0 +1,2 @@ +declare const asideMenuConfig: any[]; +export { asideMenuConfig }; diff --git a/packages/editor-skeleton/es/config/utils.js b/packages/editor-skeleton/es/config/utils.js new file mode 100644 index 000000000..39cee308b --- /dev/null +++ b/packages/editor-skeleton/es/config/utils.js @@ -0,0 +1,3 @@ +// 菜单配置 +var asideMenuConfig = []; +export { asideMenuConfig }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/global.scss b/packages/editor-skeleton/es/global.scss new file mode 100644 index 000000000..0a710b895 --- /dev/null +++ b/packages/editor-skeleton/es/global.scss @@ -0,0 +1,33 @@ +body { + font-family: PingFangSC-Regular, Roboto, Helvetica Neue, Helvetica, Tahoma, + Arial, PingFang SC-Light, Microsoft YaHei; + font-size: 12px; + padding: 0; + margin: 0; + * { + box-sizing: border-box; + } +} +.next-loading { + .next-loading-wrap { + height: 100%; + } +} +.lowcode-editor { + .lowcode-main-content { + position: absolute; + top: 48px; + left: 0; + right: 0; + bottom: 0; + display: flex; + background-color: #d8d8d8; + } + .lowcode-center-area { + flex: 1; + display: flex; + flex-direction: column; + padding: 10px; + overflow: auto; + } +} diff --git a/packages/editor-skeleton/es/index.d.ts b/packages/editor-skeleton/es/index.d.ts new file mode 100644 index 000000000..468afa29c --- /dev/null +++ b/packages/editor-skeleton/es/index.d.ts @@ -0,0 +1,8 @@ +import { PureComponent } from 'react'; +import './global.scss'; +export default class Skeleton extends PureComponent { + static displayName: string; + constructor(props: any); + componentWillUnmount(): void; + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/index.js b/packages/editor-skeleton/es/index.js new file mode 100644 index 000000000..f875f6604 --- /dev/null +++ b/packages/editor-skeleton/es/index.js @@ -0,0 +1,70 @@ +import _ConfigProvider from "@alifd/next/es/config-provider"; +import _Loading from "@alifd/next/es/loading"; +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent } from 'react'; // import Editor from '@ali/lowcode-engine-editor'; + +import TopArea from './layouts/TopArea'; +import LeftArea from './layouts/LeftArea'; +import CenterArea from './layouts/CenterArea'; +import RightArea from './layouts/RightArea'; +import './global.scss'; + +var Skeleton = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(Skeleton, _PureComponent); + + function Skeleton(props) { + var _this; + + _this = _PureComponent.call(this, props) || this; // this.editor = new Editor(props.config, props.utils); + + _this.editor = { + on: function on() {}, + off: function off() {}, + config: props.config, + pluginComponents: props.pluginComponents + }; + return _this; + } + + var _proto = Skeleton.prototype; + + _proto.componentWillUnmount = function componentWillUnmount() {// this.editor && this.editor.destroy(); + // this.editor = null; + }; + + _proto.render = function render() { + var _this$props = this.props, + location = _this$props.location, + history = _this$props.history, + messages = _this$props.messages; + this.editor.location = location; + this.editor.history = history; + this.editor.messages = messages; + return React.createElement(_ConfigProvider, null, React.createElement(_Loading, { + tip: "Loading", + size: "large", + visible: false, + shape: "fusion-reactor", + fullScreen: true + }, React.createElement("div", { + className: "lowcode-editor" + }, React.createElement(TopArea, { + editor: this.editor + }), React.createElement("div", { + className: "lowcode-main-content" + }, React.createElement(LeftArea.Nav, { + editor: this.editor + }), React.createElement(LeftArea.Panel, { + editor: this.editor + }), React.createElement(CenterArea, { + editor: this.editor + }), React.createElement(RightArea, { + editor: this.editor + }))))); + }; + + return Skeleton; +}(PureComponent); + +Skeleton.displayName = 'lowcodeEditorSkeleton'; +export { Skeleton as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/CenterArea/index.d.ts b/packages/editor-skeleton/es/layouts/CenterArea/index.d.ts new file mode 100644 index 000000000..201db137a --- /dev/null +++ b/packages/editor-skeleton/es/layouts/CenterArea/index.d.ts @@ -0,0 +1,7 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class CenterArea extends PureComponent { + static displayName: string; + constructor(props: any); + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/layouts/CenterArea/index.js b/packages/editor-skeleton/es/layouts/CenterArea/index.js new file mode 100644 index 000000000..4b9710f81 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/CenterArea/index.js @@ -0,0 +1,24 @@ +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent } from 'react'; +import './index.scss'; + +var CenterArea = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(CenterArea, _PureComponent); + + function CenterArea(props) { + return _PureComponent.call(this, props) || this; + } + + var _proto = CenterArea.prototype; + + _proto.render = function render() { + return React.createElement("div", { + className: "lowcode-center-area" + }); + }; + + return CenterArea; +}(PureComponent); + +CenterArea.displayName = 'lowcodeCenterArea'; +export { CenterArea as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/CenterArea/index.scss b/packages/editor-skeleton/es/layouts/CenterArea/index.scss new file mode 100644 index 000000000..b2584ed2b --- /dev/null +++ b/packages/editor-skeleton/es/layouts/CenterArea/index.scss @@ -0,0 +1,3 @@ +.lowcode-center-area { + padding: 12px; +} diff --git a/packages/editor-skeleton/es/layouts/LeftArea/index.d.ts b/packages/editor-skeleton/es/layouts/LeftArea/index.d.ts new file mode 100644 index 000000000..d888d90f0 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/index.d.ts @@ -0,0 +1,5 @@ +declare const _default: { + Nav: any; + Panel: any; +}; +export default _default; diff --git a/packages/editor-skeleton/es/layouts/LeftArea/index.js b/packages/editor-skeleton/es/layouts/LeftArea/index.js new file mode 100644 index 000000000..50ecfad2f --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/index.js @@ -0,0 +1,6 @@ +import Nav from './nav'; +import Panel from './panel'; +export default { + Nav: Nav, + Panel: Panel +}; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/LeftArea/index.scss b/packages/editor-skeleton/es/layouts/LeftArea/index.scss new file mode 100644 index 000000000..dac1b6b0a --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/index.scss @@ -0,0 +1,21 @@ +.lowcode-left-area-nav { + width: 48px; + height: 100%; + background: #ffffff; + border-right: 1px solid #e8ebee; + position: relative; + .top-area { + position: absolute; + top: 0; + width: 100%; + background: #ffffff; + max-height: 100%; + } + .bottom-area { + position: absolute; + bottom: 20px; + width: 100%; + background: #ffffff; + max-height: calc(100% - 20px); + } +} diff --git a/packages/editor-skeleton/es/layouts/LeftArea/nav.d.ts b/packages/editor-skeleton/es/layouts/LeftArea/nav.d.ts new file mode 100644 index 000000000..c44a09527 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/nav.d.ts @@ -0,0 +1,7 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class LeftAreaPanel extends PureComponent { + static displayName: string; + constructor(props: any); + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/layouts/LeftArea/nav.js b/packages/editor-skeleton/es/layouts/LeftArea/nav.js new file mode 100644 index 000000000..962129007 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/nav.js @@ -0,0 +1,24 @@ +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent } from 'react'; +import './index.scss'; + +var LeftAreaPanel = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(LeftAreaPanel, _PureComponent); + + function LeftAreaPanel(props) { + return _PureComponent.call(this, props) || this; + } + + var _proto = LeftAreaPanel.prototype; + + _proto.render = function render() { + return React.createElement("div", { + className: "lowcode-left-area-nav" + }); + }; + + return LeftAreaPanel; +}(PureComponent); + +LeftAreaPanel.displayName = 'lowcodeLeftAreaNav'; +export { LeftAreaPanel as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/LeftArea/panel.d.ts b/packages/editor-skeleton/es/layouts/LeftArea/panel.d.ts new file mode 100644 index 000000000..c44a09527 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/panel.d.ts @@ -0,0 +1,7 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class LeftAreaPanel extends PureComponent { + static displayName: string; + constructor(props: any); + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/layouts/LeftArea/panel.js b/packages/editor-skeleton/es/layouts/LeftArea/panel.js new file mode 100644 index 000000000..332529ce2 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/LeftArea/panel.js @@ -0,0 +1,24 @@ +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent } from 'react'; +import './index.scss'; + +var LeftAreaPanel = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(LeftAreaPanel, _PureComponent); + + function LeftAreaPanel(props) { + return _PureComponent.call(this, props) || this; + } + + var _proto = LeftAreaPanel.prototype; + + _proto.render = function render() { + return React.createElement("div", { + className: "lowcode-left-area-panel" + }); + }; + + return LeftAreaPanel; +}(PureComponent); + +LeftAreaPanel.displayName = 'lowcodeLeftAreaPanel'; +export { LeftAreaPanel as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/RightArea/index.d.ts b/packages/editor-skeleton/es/layouts/RightArea/index.d.ts new file mode 100644 index 000000000..ad87a57de --- /dev/null +++ b/packages/editor-skeleton/es/layouts/RightArea/index.d.ts @@ -0,0 +1,7 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class RightArea extends PureComponent { + static displayName: string; + constructor(props: any); + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/layouts/RightArea/index.js b/packages/editor-skeleton/es/layouts/RightArea/index.js new file mode 100644 index 000000000..d1d38689d --- /dev/null +++ b/packages/editor-skeleton/es/layouts/RightArea/index.js @@ -0,0 +1,24 @@ +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import React, { PureComponent } from 'react'; +import './index.scss'; + +var RightArea = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(RightArea, _PureComponent); + + function RightArea(props) { + return _PureComponent.call(this, props) || this; + } + + var _proto = RightArea.prototype; + + _proto.render = function render() { + return React.createElement("div", { + className: "lowcode-right-area" + }); + }; + + return RightArea; +}(PureComponent); + +RightArea.displayName = 'lowcodeRightArea'; +export { RightArea as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/RightArea/index.scss b/packages/editor-skeleton/es/layouts/RightArea/index.scss new file mode 100644 index 000000000..120ef4f11 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/RightArea/index.scss @@ -0,0 +1,157 @@ +.lowcode-right-area { + width: 300px; + height: 100%; + background-color: #ffffff; + border-left: 1px solid #e8ebee; + .right-plugin-title { + &.locked { + color: red !important; + } + &.active { + color: $color-brand1-9 !important; + } + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + } + + //tab定义 + .next-tabs-wrapped.right-tabs { + display: flex; + flex-direction: column; + margin-top: -1px; + .next-tabs-bar { + z-index: 1; + } + .next-tabs-nav { + display: block; + .next-tabs-tab { + &:first-child { + border-left: none; + } + font-size: 14px; + text-align: center; + border-right: none !important; + margin-right: 0 !important; + width: 25%; + &.active { + background: none; + border-bottom-color: #f7f7f7 !important; + } + } + } + } + .next-tabs-content { + flex: 1; + .next-tabs-tabpane.active { + height: 100%; + overflow-y: auto; + } + } + //组件 + .select-comp { + padding: 10px 16px; + line-height: 16px; + color: #989a9c; + & > span { + font-size: 12px; + line-height: 16px; + font-weight: 400; + } + & > .btn-wrap, + & > .next-btn { + width: auto; + margin: 0 5px; + float: right; + } + } + + .unselected { + padding: 60px 0; + text-align: center; + } + //右侧属性面板样式调整; + .offset-56 { + padding-left: 56px; + margin-bottom: 16px; + overflow: hidden; + } + .fixedSpan.next-form-item { + & > .next-form-item-label { + width: 56px; + flex: none; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + & > .next-form-item-control { + padding-right: 24px; + } + } + .fixedSpan.next-form-item, + .offset-56 .next-form-item { + display: flex; + & > .next-form-item-control { + width: auto; + flex: 1; + max-width: none; + .next-input, + .next-select, + .next-radio-group, + .next-number-picker, + .luna-reactnode-btn, + .luna-monaco-button button, + .luna-object-button button { + width: 100%; + } + .next-number-picker { + width: 100%; + .next-after { + padding-right: 5px; + } + } + .next-radio-group { + display: flex; + label { + flex: 1; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } + .topSpan.next-form-item { + margin-bottom: 4px; + & > .next-form-item-control { + padding-right: 24px; + .next-input, + .next-select, + .next-radio-group, + .next-number-picker, + .luna-reactnode-btn, + .luna-monaco-button button, + .luna-object-button button { + width: 100%; + } + .next-number-picker { + width: 100%; + .next-after { + padding-right: 5px; + } + } + .next-radio-group { + display: flex; + label { + flex: 1; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } +} diff --git a/packages/editor-skeleton/es/layouts/TopArea/index.d.ts b/packages/editor-skeleton/es/layouts/TopArea/index.d.ts new file mode 100644 index 000000000..e800a1e74 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/TopArea/index.d.ts @@ -0,0 +1,11 @@ +import { PureComponent } from 'react'; +import './index.scss'; +export default class TopArea extends PureComponent { + static displayName: string; + constructor(props: any); + componentDidMount(): void; + componentWillUnmount(): void; + handlePluginStatusChange: () => void; + renderPluginList: (list?: any[]) => JSX.Element[]; + render(): JSX.Element; +} diff --git a/packages/editor-skeleton/es/layouts/TopArea/index.js b/packages/editor-skeleton/es/layouts/TopArea/index.js new file mode 100644 index 000000000..c82eefc61 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/TopArea/index.js @@ -0,0 +1,83 @@ +import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; +import _Grid from "@alifd/next/es/grid"; +import React, { PureComponent } from 'react'; +import TopPlugin from '../../components/TopPlugin'; +import './index.scss'; +var Row = _Grid.Row, + Col = _Grid.Col; + +var TopArea = /*#__PURE__*/function (_PureComponent) { + _inheritsLoose(TopArea, _PureComponent); + + function TopArea(props) { + var _this; + + _this = _PureComponent.call(this, props) || this; + + _this.handlePluginStatusChange = function () {}; + + _this.renderPluginList = function (list) { + if (list === void 0) { + list = []; + } + + return list.map(function (item, idx) { + var isDivider = item.type === 'Divider'; + return React.createElement(Col, { + className: isDivider ? 'divider' : '', + key: isDivider ? idx : item.pluginKey, + style: { + width: item.props && item.props.width || 40, + flex: 'none' + } + }, !isDivider && React.createElement(TopPlugin, { + config: item, + pluginClass: _this.editor.pluginComponents[item.pluginKey], + status: _this.editor.pluginStatus[item.pluginKey] + })); + }); + }; + + _this.editor = props.editor; + _this.config = _this.editor.config.plugins && _this.editor.config.plugins.topArea; + return _this; + } + + var _proto = TopArea.prototype; + + _proto.componentDidMount = function componentDidMount() {}; + + _proto.componentWillUnmount = function componentWillUnmount() {}; + + _proto.render = function render() { + if (!this.config) return null; + var leftList = []; + var rightList = []; + this.config.forEach(function (item) { + var align = item.props && item.props.align === 'right' ? 'right' : 'left'; // 分隔符不允许相邻 + + if (item.type === 'Divider') { + var currentList = align === 'right' ? rightList : leftList; + if (currList.length === 0 || currList[currList.length - 1].type === 'Divider') return; + } + + if (align === 'right') { + rightList.push(item); + } else { + leftList.push(item); + } + }); + return React.createElement("div", { + className: "lowcode-top-area" + }, React.createElement("div", { + className: "left-area" + }, this.renderPluginList(leftList)), React.createElement("div", { + classname: "right-area" + }, this.renderPluginList(rightList))); + }; + + return TopArea; +}(PureComponent); + +TopArea.displayName = 'lowcodeTopArea'; +export { TopArea as default }; \ No newline at end of file diff --git a/packages/editor-skeleton/es/layouts/TopArea/index.scss b/packages/editor-skeleton/es/layouts/TopArea/index.scss new file mode 100644 index 000000000..ca8bbd825 --- /dev/null +++ b/packages/editor-skeleton/es/layouts/TopArea/index.scss @@ -0,0 +1,5 @@ +.lowcode-top-area { + height: 48px; + background-color: #ffffff; + border-bottom: 1px solid #e8ebee; +} diff --git a/packages/editor-skeleton/es/locale/en-US.js b/packages/editor-skeleton/es/locale/en-US.js new file mode 100644 index 000000000..f190625da --- /dev/null +++ b/packages/editor-skeleton/es/locale/en-US.js @@ -0,0 +1,10 @@ +export default { + loading: 'loading...', + rejectRedirect: 'Redirect is not allowed', + expand: 'Unfold', + fold: 'Fold', + pageNotExist: 'The current Page not exist', + enterFromAppCenter: 'Please enter from the app center', + noPermission: 'Sorry, you do not have the develop permission', + getPermission: 'Please connect the app owners {owners} to get the permission' +}; \ No newline at end of file diff --git a/packages/editor-skeleton/es/locale/ja-JP.js b/packages/editor-skeleton/es/locale/ja-JP.js new file mode 100644 index 000000000..7c645e42f --- /dev/null +++ b/packages/editor-skeleton/es/locale/ja-JP.js @@ -0,0 +1 @@ +export default {}; \ No newline at end of file diff --git a/packages/editor-skeleton/es/locale/zh-CN.js b/packages/editor-skeleton/es/locale/zh-CN.js new file mode 100644 index 000000000..51791f741 --- /dev/null +++ b/packages/editor-skeleton/es/locale/zh-CN.js @@ -0,0 +1,10 @@ +export default { + loading: '加载中...', + rejectRedirect: '开发中,已阻止发生跳转', + expand: '展开', + fold: '收起', + pageNotExist: '当前访问地址不存在', + enterFromAppCenter: '请从应用中心入口重新进入', + noPermission: '抱歉,您暂无开发权限', + getPermission: '请移步应用中心申请开发权限, 或联系 {owners} 开通权限' +}; \ No newline at end of file diff --git a/packages/editor-skeleton/es/locale/zh-TW.js b/packages/editor-skeleton/es/locale/zh-TW.js new file mode 100644 index 000000000..7c645e42f --- /dev/null +++ b/packages/editor-skeleton/es/locale/zh-TW.js @@ -0,0 +1 @@ +export default {}; \ No newline at end of file diff --git a/packages/editor-skeleton/es/style.js b/packages/editor-skeleton/es/style.js new file mode 100644 index 000000000..7f81d0018 --- /dev/null +++ b/packages/editor-skeleton/es/style.js @@ -0,0 +1,8 @@ +import '@alifd/next/es/config-provider/style'; +import '@alifd/next/es/loading/style'; +import '@alifd/next/es/grid/style'; +import '@alifd/next/es/balloon/style'; +import '@alifd/next/es/dialog/style'; +import '@alifd/next/es/badge/style'; +import '@alifd/next/es/button/style'; +import '@alifd/next/es/icon/style'; \ No newline at end of file diff --git a/packages/editor-skeleton/jsconfig.json b/packages/editor-skeleton/jsconfig.json new file mode 100644 index 000000000..9e0f3c03d --- /dev/null +++ b/packages/editor-skeleton/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "jsx": "react", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/editor-skeleton/package.json b/packages/editor-skeleton/package.json new file mode 100644 index 000000000..1320d2903 --- /dev/null +++ b/packages/editor-skeleton/package.json @@ -0,0 +1,57 @@ +{ + "name": "@ali/lowcode-engine-skeleton", + "version": "0.0.1", + "description": "alibaba lowcode editor skeleton", + "files": [ + "demo/", + "es/", + "lib/", + "build/" + ], + "main": "lib/index.tsx", + "module": "es/index.js", + "stylePath": "style.js", + "scripts": { + "start": "build-scripts start", + "build": "build-scripts build --skip-demo", + "prepublishOnly": "npm run prettier && npm run build", + "lint": "eslint --cache --ext .js,.jsx ./", + "prettier": "prettier --write \"./src/**/*.{ts,tsx,js,jsx,ejs,less,css,scss,json}\" " + }, + "keywords": [ + "lowcode", + "editor" + ], + "author": "xiayang.xy", + "dependencies": { + "@alifd/next": "^1.x", + "@icedesign/theme": "^1.x", + "@types/react": "^16.8.3", + "@types/react-dom": "^16.8.2", + "moment": "^2.23.0", + "prop-types": "^15.5.8", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router-dom": "^5.0.1" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.3", + "@alifd/next": "1.x", + "@ice/spec": "^0.1.1", + "@types/lodash": "^4.14.149", + "@types/react": "^16.9.13", + "@types/react-dom": "^16.9.4", + "build-plugin-component": "^0.2.7-1", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "prettier": "^1.19.1", + "react": "^16.8.0", + "react-dom": "^16.8.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/ice-lab/react-materials/tree/master/scaffolds/ice-ts" + }, + "homepage": "https://unpkg.alibaba-inc.com/@ali/lowcode-engine-skeleton@0.0.1/build/index.html" +} diff --git a/packages/editor-skeleton/src/components/LeftIcon/index.scss b/packages/editor-skeleton/src/components/LeftIcon/index.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor-skeleton/src/components/LeftIcon/index.tsx b/packages/editor-skeleton/src/components/LeftIcon/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor-skeleton/src/components/LeftPlugin/index.scss b/packages/editor-skeleton/src/components/LeftPlugin/index.scss new file mode 100644 index 000000000..9c6922129 --- /dev/null +++ b/packages/editor-skeleton/src/components/LeftPlugin/index.scss @@ -0,0 +1,59 @@ +.luna-left-addon { + font-size: 16px; + text-align: center; + line-height: 36px; + height: 36px; + position: relative; + cursor: pointer; + transition: all 0.3s ease; + color: #777; + &.collapse { + height: 40px; + color: #8c8c8c; + border-bottom: 1px solid #bfbfbf; + } + &.locked { + color: red !important; + } + &.active { + color: #fff !important; + background-color: $color-brand1-9 !important; + &.disabled { + color: #fff; + background-color: $color-fill1-7; + } + } + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + &:hover { + background-color: $color-brand1-1; + color: $color-brand1-6; + &:before { + content: attr(data-tooltip); + display: block; + position: absolute; + left: 50px; + top: 5px; + line-height: 18px; + font-size: 12px; + white-space: nowrap; + padding: 6px 8px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: #fff; + z-index: 100; + } + &:after { + content: ''; + display: block; + position: absolute; + left: 40px; + top: 15px; + border: 5px solid transparent; + border-right-color: rgba(0, 0, 0, 0.75); + z-index: 100; + } + } +} diff --git a/packages/editor-skeleton/src/components/LeftPlugin/index.tsx b/packages/editor-skeleton/src/components/LeftPlugin/index.tsx new file mode 100644 index 000000000..d9a637a21 --- /dev/null +++ b/packages/editor-skeleton/src/components/LeftPlugin/index.tsx @@ -0,0 +1,223 @@ +import React, { PureComponent, Fragment } from 'react'; + +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import AppContext from '@ali/iceluna-sdk/lib/context/appContext'; +import { Balloon, Dialog, Icon, Badge } from '@alife/next'; + +import './index.scss'; +export default class LeftAddon extends PureComponent { + static displayName = 'LunaLeftAddon'; + static propTypes = { + active: PropTypes.bool, + config: PropTypes.shape({ + addonKey: PropTypes.string, + addonProps: PropTypes.object, + props: PropTypes.object, + type: PropTypes.oneOf([ + 'DialogIcon', + 'BalloonIcon', + 'PanelIcon', + 'LinkIcon', + 'Icon', + 'Custom', + ]), + }), + disabled: PropTypes.bool, + dotted: PropTypes.bool, + locked: PropTypes.bool, + onClick: PropTypes.func, + }; + static defaultProps = { + active: false, + config: {}, + disabled: false, + dotted: false, + locked: false, + onClick: () => {}, + }; + static contextType = AppContext; + + constructor(props, context) { + super(props, context); + this.state = { + dialogVisible: false, + }; + this.appHelper = context.appHelper; + this.utils = this.appHelper.utils; + this.constants = this.appHelper.constants; + } + + componentDidMount() { + const { config } = this.props; + const addonKey = config && config.addonKey; + const appHelper = this.appHelper; + if (appHelper && addonKey) { + appHelper.on(`${addonKey}.dialog.show`, this.handleShow); + appHelper.on(`${addonKey}.dialog.close`, this.handleClose); + } + } + + componentWillUnmount() { + const { config } = this.props; + const appHelper = this.appHelper; + const addonKey = config && config.addonKey; + if (appHelper && addonKey) { + appHelper.off(`${addonKey}.dialog.show`, this.handleShow); + appHelper.off(`${addonKey}.dialog.close`, this.handleClose); + } + } + + handleClose = () => { + const addonKey = this.props.config && this.props.config.addonKey; + const currentAddon = + this.appHelper.addons && this.appHelper.addons[addonKey]; + if (currentAddon) { + this.utils.transformToPromise(currentAddon.close()).then(() => { + this.setState({ + dialogVisible: false, + }); + }); + } + }; + + handleOpen = () => { + // todo 对话框类型的插件初始时拿不到插件实例 + this.setState({ + dialogVisible: true, + }); + }; + + handleShow = () => { + const { disabled, config, onClick } = this.props; + const addonKey = config && config.addonKey; + if (disabled || !addonKey) return; + //考虑到弹窗情况,延时发送消息 + setTimeout(() => this.appHelper.emit(`${addonKey}.addon.activate`), 0); + this.handleOpen(); + onClick && onClick(); + }; + + renderIcon = clickCallback => { + const { active, disabled, dotted, locked, onClick, config } = this.props; + const { addonKey, props } = config || {}; + const { icon, title } = props || {}; + return ( +
{ + if (disabled) return; + //考虑到弹窗情况,延时发送消息 + clickCallback && clickCallback(); + onClick && onClick(); + }} + > + {dotted ? ( + + + + ) : ( + + )} +
+ ); + }; + + render() { + const { dotted, locked, active, disabled, config } = this.props; + const { addonKey, props, type, addonProps } = config || {}; + const { onClick, title } = props || {}; + const { dialogVisible } = this.state; + const { appHelper, components } = this.context; + if (!addonKey || !type || !props) return null; + const componentName = appHelper.utils.generateAddonCompName(addonKey); + const localeProps = {}; + const { locale, messages } = appHelper; + if (locale) { + localeProps.locale = locale; + } + if (messages && messages[componentName]) { + localeProps.messages = messages[componentName]; + } + const AddonComp = components && components[componentName]; + const node = + (AddonComp && ( + { + onClick && onClick.call(null, appHelper); + }} + {...localeProps} + {...(addonProps || {})} + /> + )) || + null; + + switch (type) { + case 'LinkIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, appHelper); + })} + + ); + case 'Icon': + return this.renderIcon(() => { + onClick && onClick.call(null, appHelper); + }); + case 'DialogIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, appHelper); + this.handleOpen(); + })} + { + appHelper.emit(`${addonKey}.dialog.onOk`); + this.handleClose(); + }} + onCancel={this.handleClose} + onClose={this.handleClose} + title={title} + {...(props.dialogProps || {})} + visible={dialogVisible} + > + {node} + + + ); + case 'BalloonIcon': + return ( + { + onClick && onClick.call(null, appHelper); + })} + align="r" + triggerType={['click', 'hover']} + {...(props.balloonProps || {})} + > + {node} + + ); + case 'PanelIcon': + return this.renderIcon(() => { + onClick && onClick.call(null, appHelper); + this.handleOpen(); + }); + case 'Custom': + return dotted ? {node} : node; + default: + return null; + } + } +} diff --git a/packages/editor-skeleton/src/components/Panel/index.tsx b/packages/editor-skeleton/src/components/Panel/index.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor-skeleton/src/components/TopIcon/index.scss b/packages/editor-skeleton/src/components/TopIcon/index.scss new file mode 100644 index 000000000..1cb3bdfdf --- /dev/null +++ b/packages/editor-skeleton/src/components/TopIcon/index.scss @@ -0,0 +1,32 @@ +.next-btn.next-large.lowcode-top-btn { + width: 44px; + height: 44px; + padding: 0; + margin: 4px -2px; + text-align: center; + border-radius: 8px; + border: 1px solid transparent; + color: #777; + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + &.locked { + color: red !important; + } + i.next-icon { + &:before { + font-size: 17px; + } + margin-right: 0; + line-height: 18px; + } + span { + display: block; + margin: 0px -5px 0; + line-height: 16px; + text-align: center; + font-size: 12px; + transform: scale(0.8); + } +} diff --git a/packages/editor-skeleton/src/components/TopIcon/index.tsx b/packages/editor-skeleton/src/components/TopIcon/index.tsx new file mode 100644 index 000000000..8e81c9c3a --- /dev/null +++ b/packages/editor-skeleton/src/components/TopIcon/index.tsx @@ -0,0 +1,68 @@ +import React, { PureComponent } from 'react'; + +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Icon, Button } from '@alifd/next'; +import './index.scss'; +export default class TopIcon extends PureComponent { + static displayName = 'TopIcon'; + static propTypes = { + active: PropTypes.bool, + className: PropTypes.string, + disabled: PropTypes.bool, + icon: PropTypes.string, + id: PropTypes.string, + locked: PropTypes.bool, + onClick: PropTypes.func, + showTitle: PropTypes.bool, + style: PropTypes.object, + title: PropTypes.string, + }; + static defaultProps = { + active: false, + className: '', + disabled: false, + icon: '', + id: '', + locked: false, + onClick: () => {}, + showTitle: false, + style: {}, + title: '', + }; + + render() { + const { + active, + disabled, + icon, + locked, + title, + className, + id, + style, + showTitle, + onClick, + } = this.props; + return ( + + ); + } +} diff --git a/packages/editor-skeleton/src/components/TopPlugin/index.scss b/packages/editor-skeleton/src/components/TopPlugin/index.scss new file mode 100644 index 000000000..4bdd7b8d2 --- /dev/null +++ b/packages/editor-skeleton/src/components/TopPlugin/index.scss @@ -0,0 +1,2 @@ +.lowcode-top-addon { +} diff --git a/packages/editor-skeleton/src/components/TopPlugin/index.tsx b/packages/editor-skeleton/src/components/TopPlugin/index.tsx new file mode 100644 index 000000000..04f5d0e59 --- /dev/null +++ b/packages/editor-skeleton/src/components/TopPlugin/index.tsx @@ -0,0 +1,174 @@ +import React, { PureComponent, Fragment } from 'react'; + +import PropTypes from 'prop-types'; +import TopIcon from '../TopIcon'; +import { Balloon, Badge, Dialog } from '@alifd/next'; + +import './index.scss'; +export default class TopPlugin extends PureComponent { + static displayName = 'lowcodeTopPlugin'; + + static defaultProps = { + active: false, + config: {}, + disabled: false, + dotted: false, + locked: false, + onClick: () => {}, + }; + + constructor(props, context) { + super(props, context); + this.state = { + dialogVisible: false, + }; + } + + componentDidMount() { + const { config } = this.props; + const pluginKey = config && config.pluginKey; + // const appHelper = this.appHelper; + // if (appHelper && addonKey) { + // appHelper.on(`${addonKey}.dialog.show`, this.handleShow); + // appHelper.on(`${addonKey}.dialog.close`, this.handleClose); + // } + } + + componentWillUnmount() { + // const { config } = this.props; + // const addonKey = config && config.addonKey; + // const appHelper = this.appHelper; + // if (appHelper && addonKey) { + // appHelper.off(`${addonKey}.dialog.show`, this.handleShow); + // appHelper.off(`${addonKey}.dialog.close`, this.handleClose); + // } + } + + handleShow = () => { + const { disabled, config, onClick } = this.props; + const addonKey = config && config.addonKey; + if (disabled || !addonKey) return; + //考虑到弹窗情况,延时发送消息 + setTimeout(() => this.appHelper.emit(`${addonKey}.addon.activate`), 0); + this.handleOpen(); + onClick && onClick(); + }; + + handleClose = () => { + const addonKey = this.props.config && this.props.config.addonKey; + const currentAddon = + this.appHelper.addons && this.appHelper.addons[addonKey]; + if (currentAddon) { + this.utils.transformToPromise(currentAddon.close()).then(() => { + this.setState({ + dialogVisible: false, + }); + }); + } + }; + + handleOpen = () => { + // todo dialog类型的插件初始时拿不动插件实例 + this.setState({ + dialogVisible: true, + }); + }; + + renderIcon = clickCallback => { + const { active, disabled, dotted, locked, config, onClick } = this.props; + const { pluginKey, props } = config || {}; + const { icon, title } = props || {}; + const node = ( + { + if (disabled) return; + //考虑到弹窗情况,延时发送消息 + setTimeout( + () => this.appHelper.emit(`${pluginKey}.addon.activate`), + 0, + ); + clickCallback && clickCallback(); + onClick && onClick(); + }} + /> + ); + return dotted ? {node} : node; + }; + + render() { + const { active, dotted, locked, disabled, config, editor, pluginClass: Comp } = this.props; + const { pluginKey, pluginProps, props, type } = config || {}; + const { onClick, title } = props || {}; + const { dialogVisible } = this.state; + if (!pluginKey || !type || !Comp) return null; + const node = { + onClick && onClick.call(null, editor); + }} + {...pluginProps} + />; + + switch (type) { + case 'LinkIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, editor); + })} + + ); + case 'Icon': + return this.renderIcon(() => { + onClick && onClick.call(null, editor); + }); + case 'DialogIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, editor); + this.handleOpen(); + })} + { + editor.emit(`${pluginKey}.dialog.onOk`); + this.handleClose(); + }} + onCancel={this.handleClose} + onClose={this.handleClose} + title={title} + {...props.dialogProps} + visible={dialogVisible} + > + {node} + + + ); + case 'BalloonIcon': + return ( + { + onClick && onClick.call(null, editor); + })} + triggerType={['click', 'hover']} + {...props.balloonProps} + > + {node} + + ); + case 'Custom': + return dotted ? {node} : node; + default: + return null; + } + } +} diff --git a/packages/editor-skeleton/src/config/skeleton.ts b/packages/editor-skeleton/src/config/skeleton.ts new file mode 100644 index 000000000..9e3f6898f --- /dev/null +++ b/packages/editor-skeleton/src/config/skeleton.ts @@ -0,0 +1,21 @@ +import Dashboard from '@/pages/Dashboard'; +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '/dashboard', + component: Dashboard, + }, + { + path: '/', + redirect: '/dashboard', + }, + ], + }, +]; + +export default routerConfig; diff --git a/packages/editor-skeleton/src/config/utils.ts b/packages/editor-skeleton/src/config/utils.ts new file mode 100644 index 000000000..e3c4c9e37 --- /dev/null +++ b/packages/editor-skeleton/src/config/utils.ts @@ -0,0 +1,5 @@ +// 菜单配置 + +const asideMenuConfig = []; + +export { asideMenuConfig }; diff --git a/packages/editor-skeleton/src/global.scss b/packages/editor-skeleton/src/global.scss new file mode 100644 index 000000000..0a710b895 --- /dev/null +++ b/packages/editor-skeleton/src/global.scss @@ -0,0 +1,33 @@ +body { + font-family: PingFangSC-Regular, Roboto, Helvetica Neue, Helvetica, Tahoma, + Arial, PingFang SC-Light, Microsoft YaHei; + font-size: 12px; + padding: 0; + margin: 0; + * { + box-sizing: border-box; + } +} +.next-loading { + .next-loading-wrap { + height: 100%; + } +} +.lowcode-editor { + .lowcode-main-content { + position: absolute; + top: 48px; + left: 0; + right: 0; + bottom: 0; + display: flex; + background-color: #d8d8d8; + } + .lowcode-center-area { + flex: 1; + display: flex; + flex-direction: column; + padding: 10px; + overflow: auto; + } +} diff --git a/packages/editor-skeleton/src/index.tsx b/packages/editor-skeleton/src/index.tsx new file mode 100644 index 000000000..3f1c89522 --- /dev/null +++ b/packages/editor-skeleton/src/index.tsx @@ -0,0 +1,60 @@ +import React, { PureComponent } from 'react'; + +// import Editor from '@ali/lowcode-engine-editor'; +import { Loading, ConfigProvider } from '@alifd/next'; +import defaultConfig from './config/skeleton'; + +import TopArea from './layouts/TopArea'; +import LeftArea from './layouts/LeftArea'; +import CenterArea from './layouts/CenterArea'; +import RightArea from './layouts/RightArea'; + +import './global.scss'; + +export default class Skeleton extends PureComponent { + static displayName = 'lowcodeEditorSkeleton'; + + constructor(props) { + super(props); + // this.editor = new Editor(props.config, props.utils); + this.editor = { + on: () => {}, + off: () => {}, + config: props.config, + pluginComponents: props.pluginComponents + }; + } + + componentWillUnmount() { + // this.editor && this.editor.destroy(); + // this.editor = null; + } + + render() { + const { location, history, messages } = this.props; + this.editor.location = location; + this.editor.history = history; + this.editor.messages = messages; + return ( + + +
+ +
+ + + + +
+
+
+
+ ); + } +} diff --git a/packages/editor-skeleton/src/layouts/CenterArea/index.scss b/packages/editor-skeleton/src/layouts/CenterArea/index.scss new file mode 100644 index 000000000..b2584ed2b --- /dev/null +++ b/packages/editor-skeleton/src/layouts/CenterArea/index.scss @@ -0,0 +1,3 @@ +.lowcode-center-area { + padding: 12px; +} diff --git a/packages/editor-skeleton/src/layouts/CenterArea/index.tsx b/packages/editor-skeleton/src/layouts/CenterArea/index.tsx new file mode 100644 index 000000000..dc2b38d25 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/CenterArea/index.tsx @@ -0,0 +1,15 @@ +import React, { PureComponent } from 'react'; + +import './index.scss'; + +export default class CenterArea extends PureComponent { + static displayName = 'lowcodeCenterArea'; + + constructor(props) { + super(props); + } + + render() { + return
; + } +} diff --git a/packages/editor-skeleton/src/layouts/LeftArea/index.scss b/packages/editor-skeleton/src/layouts/LeftArea/index.scss new file mode 100644 index 000000000..dac1b6b0a --- /dev/null +++ b/packages/editor-skeleton/src/layouts/LeftArea/index.scss @@ -0,0 +1,21 @@ +.lowcode-left-area-nav { + width: 48px; + height: 100%; + background: #ffffff; + border-right: 1px solid #e8ebee; + position: relative; + .top-area { + position: absolute; + top: 0; + width: 100%; + background: #ffffff; + max-height: 100%; + } + .bottom-area { + position: absolute; + bottom: 20px; + width: 100%; + background: #ffffff; + max-height: calc(100% - 20px); + } +} diff --git a/packages/editor-skeleton/src/layouts/LeftArea/index.tsx b/packages/editor-skeleton/src/layouts/LeftArea/index.tsx new file mode 100644 index 000000000..805a5e014 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/LeftArea/index.tsx @@ -0,0 +1,7 @@ +import Nav from './nav'; +import Panel from './panel'; + +export default { + Nav, + Panel, +}; diff --git a/packages/editor-skeleton/src/layouts/LeftArea/nav.tsx b/packages/editor-skeleton/src/layouts/LeftArea/nav.tsx new file mode 100644 index 000000000..6c58c12ef --- /dev/null +++ b/packages/editor-skeleton/src/layouts/LeftArea/nav.tsx @@ -0,0 +1,15 @@ +import React, { PureComponent } from 'react'; + +import './index.scss'; + +export default class LeftAreaPanel extends PureComponent { + static displayName = 'lowcodeLeftAreaNav'; + + constructor(props) { + super(props); + } + + render() { + return
; + } +} diff --git a/packages/editor-skeleton/src/layouts/LeftArea/panel.tsx b/packages/editor-skeleton/src/layouts/LeftArea/panel.tsx new file mode 100644 index 000000000..ab41860fb --- /dev/null +++ b/packages/editor-skeleton/src/layouts/LeftArea/panel.tsx @@ -0,0 +1,15 @@ +import React, { PureComponent } from 'react'; + +import './index.scss'; + +export default class LeftAreaPanel extends PureComponent { + static displayName = 'lowcodeLeftAreaPanel'; + + constructor(props) { + super(props); + } + + render() { + return
; + } +} diff --git a/packages/editor-skeleton/src/layouts/RightArea/index.scss b/packages/editor-skeleton/src/layouts/RightArea/index.scss new file mode 100644 index 000000000..120ef4f11 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/RightArea/index.scss @@ -0,0 +1,157 @@ +.lowcode-right-area { + width: 300px; + height: 100%; + background-color: #ffffff; + border-left: 1px solid #e8ebee; + .right-plugin-title { + &.locked { + color: red !important; + } + &.active { + color: $color-brand1-9 !important; + } + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + } + + //tab定义 + .next-tabs-wrapped.right-tabs { + display: flex; + flex-direction: column; + margin-top: -1px; + .next-tabs-bar { + z-index: 1; + } + .next-tabs-nav { + display: block; + .next-tabs-tab { + &:first-child { + border-left: none; + } + font-size: 14px; + text-align: center; + border-right: none !important; + margin-right: 0 !important; + width: 25%; + &.active { + background: none; + border-bottom-color: #f7f7f7 !important; + } + } + } + } + .next-tabs-content { + flex: 1; + .next-tabs-tabpane.active { + height: 100%; + overflow-y: auto; + } + } + //组件 + .select-comp { + padding: 10px 16px; + line-height: 16px; + color: #989a9c; + & > span { + font-size: 12px; + line-height: 16px; + font-weight: 400; + } + & > .btn-wrap, + & > .next-btn { + width: auto; + margin: 0 5px; + float: right; + } + } + + .unselected { + padding: 60px 0; + text-align: center; + } + //右侧属性面板样式调整; + .offset-56 { + padding-left: 56px; + margin-bottom: 16px; + overflow: hidden; + } + .fixedSpan.next-form-item { + & > .next-form-item-label { + width: 56px; + flex: none; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + & > .next-form-item-control { + padding-right: 24px; + } + } + .fixedSpan.next-form-item, + .offset-56 .next-form-item { + display: flex; + & > .next-form-item-control { + width: auto; + flex: 1; + max-width: none; + .next-input, + .next-select, + .next-radio-group, + .next-number-picker, + .luna-reactnode-btn, + .luna-monaco-button button, + .luna-object-button button { + width: 100%; + } + .next-number-picker { + width: 100%; + .next-after { + padding-right: 5px; + } + } + .next-radio-group { + display: flex; + label { + flex: 1; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } + .topSpan.next-form-item { + margin-bottom: 4px; + & > .next-form-item-control { + padding-right: 24px; + .next-input, + .next-select, + .next-radio-group, + .next-number-picker, + .luna-reactnode-btn, + .luna-monaco-button button, + .luna-object-button button { + width: 100%; + } + .next-number-picker { + width: 100%; + .next-after { + padding-right: 5px; + } + } + .next-radio-group { + display: flex; + label { + flex: 1; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } +} diff --git a/packages/editor-skeleton/src/layouts/RightArea/index.tsx b/packages/editor-skeleton/src/layouts/RightArea/index.tsx new file mode 100644 index 000000000..31273a4e2 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/RightArea/index.tsx @@ -0,0 +1,15 @@ +import React, { PureComponent } from 'react'; + +import './index.scss'; + +export default class RightArea extends PureComponent { + static displayName = 'lowcodeRightArea'; + + constructor(props) { + super(props); + } + + render() { + return
; + } +} diff --git a/packages/editor-skeleton/src/layouts/TopArea/index.scss b/packages/editor-skeleton/src/layouts/TopArea/index.scss new file mode 100644 index 000000000..ca8bbd825 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/TopArea/index.scss @@ -0,0 +1,5 @@ +.lowcode-top-area { + height: 48px; + background-color: #ffffff; + border-bottom: 1px solid #e8ebee; +} diff --git a/packages/editor-skeleton/src/layouts/TopArea/index.tsx b/packages/editor-skeleton/src/layouts/TopArea/index.tsx new file mode 100644 index 000000000..c2f60a637 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/TopArea/index.tsx @@ -0,0 +1,79 @@ +import React, { PureComponent } from 'react'; +import { Grid } from '@alifd/next'; +import TopPlugin from '../../components/TopPlugin'; +import './index.scss'; + +const { Row, Col } = Grid; + +export default class TopArea extends PureComponent { + static displayName = 'lowcodeTopArea'; + + constructor(props) { + super(props); + this.editor = props.editor; + this.config = this.editor.config.plugins && this.editor.config.plugins.topArea; + } + + componentDidMount() { + } + componentWillUnmount() { + } + + handlePluginStatusChange = () => {}; + + renderPluginList = (list = []) => { + return list.map((item, idx) => { + const isDivider = item.type === 'Divider'; + + return ( + + {!isDivider && ( + + )} + + ); + }); + }; + + render() { + if (!this.config) return null; + const leftList = []; + const rightList = []; + this.config.forEach(item => { + const align = + item.props && item.props.align === 'right' ? 'right' : 'left'; + // 分隔符不允许相邻 + if (item.type === 'Divider') { + const currentList = align === 'right' ? rightList : leftList; + if ( + currList.length === 0 || + currList[currList.length - 1].type === 'Divider' + ) + return; + } + if (align === 'right') { + rightList.push(item); + } else { + leftList.push(item); + } + }); + + return ( +
+
{this.renderPluginList(leftList)}
+
{this.renderPluginList(rightList)}
+
+ ); + } +} diff --git a/packages/editor-skeleton/src/locale/en-US.js b/packages/editor-skeleton/src/locale/en-US.js new file mode 100644 index 000000000..36e3b219c --- /dev/null +++ b/packages/editor-skeleton/src/locale/en-US.js @@ -0,0 +1,10 @@ +export default { + loading: 'loading...', + rejectRedirect: 'Redirect is not allowed', + expand: 'Unfold', + fold: 'Fold', + pageNotExist: 'The current Page not exist', + enterFromAppCenter: 'Please enter from the app center', + noPermission: 'Sorry, you do not have the develop permission', + getPermission: 'Please connect the app owners {owners} to get the permission', +}; diff --git a/packages/editor-skeleton/src/locale/ja-JP.js b/packages/editor-skeleton/src/locale/ja-JP.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor-skeleton/src/locale/ja-JP.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor-skeleton/src/locale/zh-CN.js b/packages/editor-skeleton/src/locale/zh-CN.js new file mode 100644 index 000000000..2d5229d2c --- /dev/null +++ b/packages/editor-skeleton/src/locale/zh-CN.js @@ -0,0 +1,10 @@ +export default { + loading: '加载中...', + rejectRedirect: '开发中,已阻止发生跳转', + expand: '展开', + fold: '收起', + pageNotExist: '当前访问地址不存在', + enterFromAppCenter: '请从应用中心入口重新进入', + noPermission: '抱歉,您暂无开发权限', + getPermission: '请移步应用中心申请开发权限, 或联系 {owners} 开通权限', +}; diff --git a/packages/editor-skeleton/src/locale/zh-TW.js b/packages/editor-skeleton/src/locale/zh-TW.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor-skeleton/src/locale/zh-TW.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor-skeleton/tests/index.js b/packages/editor-skeleton/tests/index.js new file mode 100644 index 000000000..346e384d2 --- /dev/null +++ b/packages/editor-skeleton/tests/index.js @@ -0,0 +1 @@ +// test file diff --git a/packages/editor-skeleton/tsconfig.json b/packages/editor-skeleton/tsconfig.json new file mode 100644 index 000000000..a511d68ba --- /dev/null +++ b/packages/editor-skeleton/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": false, + "buildOnSave": false, + "compilerOptions": { + "outDir": "build", + "module": "esnext", + "target": "es6", + "jsx": "react", + "moduleResolution": "node", + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": true, + "skipLibCheck": true + }, + "include": ["src/*.ts", "src/*.tsx"], + "exclude": ["node_modules", "build", "public"] +} diff --git a/packages/editor/.editorconfig b/packages/editor/.editorconfig new file mode 100644 index 000000000..5760be583 --- /dev/null +++ b/packages/editor/.editorconfig @@ -0,0 +1,12 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/packages/editor/.eslintignore b/packages/editor/.eslintignore new file mode 100644 index 000000000..3b437e614 --- /dev/null +++ b/packages/editor/.eslintignore @@ -0,0 +1,11 @@ +# 忽略目录 +build/ +tests/ +demo/ + +# node 覆盖率文件 +coverage/ + +# 忽略文件 +**/*-min.js +**/*.min.js diff --git a/packages/editor/.eslintrc.js b/packages/editor/.eslintrc.js new file mode 100644 index 000000000..18ae6baa7 --- /dev/null +++ b/packages/editor/.eslintrc.js @@ -0,0 +1,7 @@ +const { eslint, deepmerge } = require('@ice/spec'); + +module.exports = deepmerge(eslint, { + rules: { + "global-require": 0, + }, +}); diff --git a/packages/editor/.gitignore b/packages/editor/.gitignore new file mode 100644 index 000000000..c590da5b0 --- /dev/null +++ b/packages/editor/.gitignore @@ -0,0 +1,20 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# production +/build +/dist + +# misc +.idea/ +.happypack +.DS_Store + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# ignore d.ts auto generated by css-modules-typescript-loader +*.module.scss.d.ts \ No newline at end of file diff --git a/packages/editor/.prettierrc b/packages/editor/.prettierrc new file mode 100644 index 000000000..d3c963559 --- /dev/null +++ b/packages/editor/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "singleQuote": true +} \ No newline at end of file diff --git a/packages/editor/.stylelintignore b/packages/editor/.stylelintignore new file mode 100644 index 000000000..82af6f60d --- /dev/null +++ b/packages/editor/.stylelintignore @@ -0,0 +1,7 @@ +# 忽略目录 +build/ +tests/ +demo/ + +# node 覆盖率文件 +coverage/ diff --git a/packages/editor/.stylelintrc.js b/packages/editor/.stylelintrc.js new file mode 100644 index 000000000..eeb605b33 --- /dev/null +++ b/packages/editor/.stylelintrc.js @@ -0,0 +1,3 @@ +const { stylelint } = require('@ice/spec'); + +module.exports = stylelint; diff --git a/packages/editor/README.md b/packages/editor/README.md new file mode 100644 index 000000000..6a1a5d0fa --- /dev/null +++ b/packages/editor/README.md @@ -0,0 +1,20 @@ +# ice-typescript-starter + +## 使用 + +- 启动调试服务: `npm start` +- 构建 dist: `npm run build` + +## 目录结构 + +- 入口文件: `src/index.jsx` +- 导航配置: `src/config/menu.js` +- 路由配置: `src/config/routes.js` +- 路由入口: `src/router.jsx` +- 布局文件: `src/layouts` +- 通用组件: `src/components` +- 页面文件: `src/pages` + +## 效果图 + +![screenshot](https://img.alicdn.com/tfs/TB13AFlH6TpK1RjSZKPXXa3UpXa-2860-1580.png) diff --git a/packages/editor/abc.json b/packages/editor/abc.json new file mode 100644 index 000000000..dce1f92ed --- /dev/null +++ b/packages/editor/abc.json @@ -0,0 +1,4 @@ +{ + "type": "ice-scripts", + "builder": "@ali/builder-ice-scripts" +} diff --git a/packages/editor/ice.config.js b/packages/editor/ice.config.js new file mode 100644 index 000000000..b40ce581a --- /dev/null +++ b/packages/editor/ice.config.js @@ -0,0 +1,30 @@ +const path = require('path'); + +module.exports = { + entry: 'src/index.tsx', + publicPath: './', + alias: { + '@': path.resolve(__dirname, './src'), + }, + plugins: [ + ['ice-plugin-fusion', { + themePackage: '@alife/dpl-iceluna', + }], + ['ice-plugin-moment-locales', { + locales: ['zh-cn'], + }], + ], + chainWebpack: (config) => { + // 修改对应 css module的 loader,默认修改 scss-module 同理可以修改 css-module 和 less-module 规则 + ['scss-module'].forEach((rule) => { + if (config.module.rules.get(rule)) { + config.module.rule(rule).use('ts-css-module-loader') + .loader(require.resolve('css-modules-typescript-loader')) + .options({ modules: true, sass: true }); + // 指定应用loader的位置 + config.module.rule(rule).use('ts-css-module-loader').before('css-loader'); + } + }); + }, +}; + diff --git a/packages/editor/jsconfig.json b/packages/editor/jsconfig.json new file mode 100644 index 000000000..9e0f3c03d --- /dev/null +++ b/packages/editor/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "jsx": "react", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/editor/package.json b/packages/editor/package.json new file mode 100644 index 000000000..3ebc396ef --- /dev/null +++ b/packages/editor/package.json @@ -0,0 +1,58 @@ +{ + "name": "@icedesign/ts-scaffold", + "version": "0.0.1", + "description": "低代码编辑器", + "dependencies": { + "@ali/iceluna-addon-2": "^1.0.3", + "@ali/iceluna-sdk": "^1.0.5-beta.26", + "@alifd/next": "^1.x", + "@alife/dpl-iceluna": "^2.3.2", + "@icedesign/theme": "^1.x", + "@types/react": "^16.8.3", + "@types/react-dom": "^16.8.2", + "events": "^3.1.0", + "intl-messageformat": "^8.2.1", + "keymaster": "^1.6.2", + "moment": "^2.23.0", + "prop-types": "^15.5.8", + "react": "^16.8.1", + "react-dom": "^16.8.1", + "react-router-dom": "^5.1.2", + "store": "^2.0.12" + }, + "devDependencies": { + "@ice/spec": "^0.1.1", + "@types/debug": "^4.1.5", + "@types/events": "^3.0.0", + "@types/store": "^2.0.2", + "css-modules-typescript-loader": "^2.0.4", + "eslint": "^6.0.1", + "ice-plugin-fusion": "^0.1.4", + "ice-plugin-moment-locales": "^0.1.0", + "ice-scripts": "^2.0.0", + "prettier": "^1.19.1", + "stylelint": "^10.1.0" + }, + "scripts": { + "start": "ice-scripts dev", + "build": "ice-scripts build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss", + "prettier": "prettier --write \"./src/**/*.{ts,tsx,js,jsx,ejs,less,css,scss,json}\" " + }, + "engines": { + "node": ">=8.0.0" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "ideMode": { + "name": "ice-react" + }, + "repository": { + "type": "git", + "url": "https://github.com/ice-lab/react-materials/tree/master/scaffolds/ice-ts" + } +} diff --git a/packages/editor/public/favicon.png b/packages/editor/public/favicon.png new file mode 100644 index 000000000..a2605c57e Binary files /dev/null and b/packages/editor/public/favicon.png differ diff --git a/packages/editor/public/index.html b/packages/editor/public/index.html new file mode 100644 index 000000000..5c0692966 --- /dev/null +++ b/packages/editor/public/index.html @@ -0,0 +1,13 @@ + + + + + + + ICE TypeScript Starter + + + +
+ + diff --git a/packages/editor/src/config/components.js b/packages/editor/src/config/components.js new file mode 100644 index 000000000..fe508c3e0 --- /dev/null +++ b/packages/editor/src/config/components.js @@ -0,0 +1,31 @@ +import logo from '../plugins/logo'; +import designer from '../plugins/designer'; +import undoRedo from '../plugins/undoRedo'; +import topBalloonIcon from '@ali/iceluna-addon-2'; +import topDialogIcon from '@ali/iceluna-addon-2'; +import leftPanelIcon from '@ali/iceluna-addon-2'; +import leftPanelIcon2 from '@ali/iceluna-addon-2'; +import leftBalloonIcon from '@ali/iceluna-addon-2'; +import leftDialogIcon from '@ali/iceluna-addon-2'; +import rightPanel1 from '@ali/iceluna-addon-2'; +import rightPanel2 from '@ali/iceluna-addon-2'; +import rightPanel3 from '@ali/iceluna-addon-2'; +import rightPanel4 from '@ali/iceluna-addon-2'; + +import PluginFactory from '../framework/pluginFactory'; + +export default { + logo: PluginFactory(logo), + designer: PluginFactory(designer), + undoRedo: PluginFactory(undoRedo), + topBalloonIcon: PluginFactory(topBalloonIcon), + topDialogIcon: PluginFactory(topDialogIcon), + leftPanelIcon: PluginFactory(leftPanelIcon), + leftPanelIcon2: PluginFactory(leftPanelIcon2), + leftBalloonIcon: PluginFactory(leftBalloonIcon), + leftDialogIcon: PluginFactory(leftDialogIcon), + rightPanel1: PluginFactory(rightPanel1), + rightPanel2: PluginFactory(rightPanel2), + rightPanel3: PluginFactory(rightPanel3), + rightPanel4: PluginFactory(rightPanel4) +}; diff --git a/packages/editor/src/config/constants.js b/packages/editor/src/config/constants.js new file mode 100644 index 000000000..d7e16fed3 --- /dev/null +++ b/packages/editor/src/config/constants.js @@ -0,0 +1,3 @@ +export default { + namespace: 'page' +}; diff --git a/packages/editor/src/config/locale/en-US.js b/packages/editor/src/config/locale/en-US.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/config/locale/en-US.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/config/locale/index.js b/packages/editor/src/config/locale/index.js new file mode 100644 index 000000000..f4ad6c5da --- /dev/null +++ b/packages/editor/src/config/locale/index.js @@ -0,0 +1,10 @@ +import en_us from './en-US'; +import zh_cn from './zh-CN'; +import zh_tw from './zh-TW'; +import ja_jp from './ja-JP'; +export default { + 'en-US': en_us, + 'zh-CN': zh_cn, + 'zh-TW': zh_tw, + 'ja-JP': ja_jp +}; diff --git a/packages/editor/src/config/locale/ja-JP.js b/packages/editor/src/config/locale/ja-JP.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/config/locale/ja-JP.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/config/locale/zh-CN.js b/packages/editor/src/config/locale/zh-CN.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/config/locale/zh-CN.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/config/locale/zh-TW.js b/packages/editor/src/config/locale/zh-TW.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/config/locale/zh-TW.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/config/skeleton.js b/packages/editor/src/config/skeleton.js new file mode 100644 index 000000000..765aa6c10 --- /dev/null +++ b/packages/editor/src/config/skeleton.js @@ -0,0 +1,266 @@ +export default { + version: '^1.0.2', + theme: { + dpl: { + package: '@alife/dpl-iceluna', + version: '^2.3.0' + }, + scss: '' + }, + constants: { + namespace: 'page' + }, + utils: [], + plugins: { + topArea: [ + { + pluginKey: 'logo', + type: 'Custom', + props: { + align: 'left', + width: 100 + }, + config: { + package: '@ali/lowcode-plugin-logo', + version: '1.0.0' + }, + pluginProps: { + logo: 'https://img.alicdn.com/tfs/TB1mHYDxQP2gK0jSZPxXXacQpXa-112-64.png' + } + }, + { + pluginKey: 'topBalloonIcon', + type: 'BalloonIcon', + props: { + align: 'left', + title: 'balloon', + icon: 'dengpao', + balloonProps: { + triggerType: 'click' + } + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'divider', + type: 'Divider', + props: { + align: 'left' + } + }, + { + pluginKey: 'topDialogIcon', + type: 'DialogIcon', + props: { + align: 'left', + title: 'dialog', + icon: 'dengpao' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'undoRedo', + type: 'Custom', + props: { + align: 'right', + width: 90 + }, + config: { + package: '@ali/lowcode-plugin-undo-redo', + version: '1.0.0' + } + }, + { + pluginKey: 'divider', + type: 'Divider', + props: { + align: 'right' + } + }, + { + pluginKey: 'topLinkIcon', + type: 'LinkIcon', + props: { + align: 'right', + title: 'link', + icon: 'dengpao', + linkProps: { + href: '//www.taobao.com', + target: 'blank' + } + }, + config: {}, + pluginProps: {} + }, + { + pluginKey: 'topIcon', + type: 'Icon', + props: { + align: 'right', + title: 'icon', + icon: 'dengpao', + onClick: function(editor) { + alert('icon addon invoke, current activeKey: ' + editor.activeKey); + } + }, + config: {}, + pluginProps: {} + } + ], + leftArea: [ + { + pluginKey: 'leftPanelIcon', + type: 'PanelIcon', + props: { + align: 'top', + title: 'panel', + icon: 'dengpao' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'leftBalloonIcon', + type: 'BalloonIcon', + props: { + align: 'top', + title: 'balloon', + icon: 'dengpao' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'leftPanelIcon2', + type: 'PanelIcon', + props: { + align: 'top', + title: 'panel2', + icon: 'dengpao' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'leftDialogIcon', + type: 'DialogIcon', + props: { + align: 'bottom', + title: 'dialog', + icon: 'dengpao' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'leftLinkIcon', + type: 'LinkIcon', + props: { + align: 'bottom', + title: 'link', + icon: 'dengpao', + linkProps: { + href: '//www.taobao.com', + target: 'blank' + } + }, + config: {}, + pluginProps: {} + }, + { + pluginKey: 'leftIcon', + type: 'Icon', + props: { + align: 'bottom', + title: 'icon', + icon: 'dengpao', + onClick: function(editor) { + alert('icon addon invoke, current activeKey: ' + editor.activeKey); + } + }, + config: {}, + pluginProps: {} + } + ], + rightArea: [ + { + pluginKey: 'rightPanel1', + type: 'Panel', + props: { + title: '样式' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'rightPanel2', + type: 'Panel', + props: { + title: '属性', + icon: 'dengpao' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'rightPanel3', + type: 'Panel', + props: { + title: '事件' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + }, + { + pluginKey: 'rightPanel4', + type: 'Panel', + props: { + title: '数据' + }, + config: { + package: '@ali/iceluna-addon-2', + version: '^1.0.0' + }, + pluginProps: {} + } + ], + centerArea: [{ + pluginKey: 'designer', + config: { + package: '@ali/lowcode-plugin-designer', + version: '1.0.0' + } + }] + }, + hooks: [], + shortCuts: [] +}; diff --git a/packages/editor/src/config/theme.scss b/packages/editor/src/config/theme.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor/src/config/utils.js b/packages/editor/src/config/utils.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/config/utils.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/framework/areaManager.ts b/packages/editor/src/framework/areaManager.ts new file mode 100644 index 000000000..6dd029b45 --- /dev/null +++ b/packages/editor/src/framework/areaManager.ts @@ -0,0 +1,32 @@ +import Editor from './index'; +import { PluginConfig, PluginStatus } from './definitions'; +import { clone, deepEqual, transformToPromise } from './utils'; + +export default class AreaManager { + private pluginStatus: PluginStatus; + private config: Array; + constructor(private editor: Editor, private area: string) { + this.config = (editor && editor.config && editor.config.plugins && editor.config.plugins[this.area]) || []; + this.pluginStatus = clone(editor.pluginStatus); + } + + isPluginStatusUpdate(pluginType?: string): boolean { + const { pluginStatus } = this.editor; + const list = pluginType ? this.config.filter(item => item.type === pluginType) : this.config; + + const isUpdate = list.some(item => !deepEqual(pluginStatus[item.pluginKey], this.pluginStatus[item.pluginKey])); + this.pluginStatus = clone(pluginStatus); + return isUpdate; + } + + getVisiblePluginList(pluginType?: string): Array { + const res = this.config.filter(item => { + return !this.pluginStatus[item.pluginKey] || this.pluginStatus[item.pluginKey].visible; + }); + return pluginType ? res.filter(item => item.type === pluginType) : res; + } + + getPluginConfig(): Array { + return this.config; + } +} diff --git a/packages/editor/src/framework/context.ts b/packages/editor/src/framework/context.ts new file mode 100644 index 000000000..78d3ce177 --- /dev/null +++ b/packages/editor/src/framework/context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; +const context = createContext({}); +export default context; diff --git a/packages/editor/src/framework/definitions.ts b/packages/editor/src/framework/definitions.ts new file mode 100644 index 000000000..5e30a4466 --- /dev/null +++ b/packages/editor/src/framework/definitions.ts @@ -0,0 +1,132 @@ +import * as React from 'react'; +import Editor from './editor'; + +export interface EditorConfig { + skeleton?: SkeletonConfig; + theme?: ThemeConfig; + plugins?: PluginsConfig; + hooks?: HooksConfig; + shortCuts?: ShortCutsConfig; + utils?: UtilsConfig; + constants?: ConstantsConfig; + lifeCycles?: lifeCyclesConfig; + i18n?: I18nConfig; +} + +export interface NpmConfig { + version: string; + package: string; + main?: string; + exportName?: string; + subName?: string; + destructuring?: boolean; +} + +export interface SkeletonConfig { + config: NpmConfig; + props?: object; + handler?: (EditorConfig) => EditorConfig; +} + +export interface FusionTheme { + package: string; + version: string; +} + +export interface ThemeConfig { + fusion?: FusionTheme; +} + +export interface PluginsConfig { + [propName: string]: Array; +} + +export interface PluginConfig { + pluginKey: string; + type: string; + props: { + icon?: string; + title?: string; + width?: number; + height?: number; + visible?: boolean; + disabled?: boolean; + marked?: boolean; + align?: 'left' | 'right' | 'top' | 'bottom'; + onClick?: () => void; + dialogProps?: object; + balloonProps?: object; + panelProps?: object; + linkProps?: object; + }; + config?: NpmConfig; + pluginProps?: object; +} + +export type HooksConfig = Array; + +export interface HookConfig { + message: string; + type: 'on' | 'once'; + handler: (editor: Editor, ...args) => void; +} + +export type ShortCutsConfig = Array; + +export interface ShortCutConfig { + keyboard: string; + handler: (editor: Editor, ev: React.KeyboardEventHandler, keymaster: any) => void; +} + +export type UtilsConfig = Array; + +export interface UtilConfig { + name: string; + type: 'npm' | 'function'; + content: NpmConfig | ((...args) => any); +} + +export type ConstantsConfig = object; + +export interface lifeCyclesConfig { + init?: (editor: Editor) => any; + destroy?: (editor: Editor) => any; +} + +export type LocaleType = 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP'; + +export interface I18nMessages { + [propName: string]: string; +} + +export interface I18nConfig { + 'zh-CN'?: I18nMessages; + 'zh-TW'?: I18nMessages; + 'en-US'?: I18nMessages; + 'ja-JP'?: I18nMessages; +} + +export type I18nFunction = (key: string, params: object) => string; + +export interface Utils { + [propName: string]: (...args) => any; +} + +export interface PluginClass extends React.Component { + init?: (editor: Editor) => void; + open?: () => any; + close?: () => any; +} + +export interface PluginComponents { + [propName: string]: PluginClass; +} + +export interface PluginStatus { + [propName: string]: { + disabled: boolean; + visible: boolean; + marked: boolean; + locked: boolean; + }; +} diff --git a/packages/editor/src/framework/editor.ts b/packages/editor/src/framework/editor.ts new file mode 100644 index 000000000..4918a4f4b --- /dev/null +++ b/packages/editor/src/framework/editor.ts @@ -0,0 +1,184 @@ +import EventEmitter from 'events'; +import Debug from 'debug'; +import store from 'store'; +import { EditorConfig, Utils, PluginComponents, PluginStatus, LocaleType, HooksConfig } from './definitions'; + +import { unRegistShortCuts, registShortCuts, transformToPromise } from './utils'; + +// 根据url参数设置debug选项 +const res = /_?debug=(.*?)(&|$)/.exec(location.search); +if (res && res[1]) { + window.__isDebug = true; + store.storage.write('debug', res[1] === 'true' ? '*' : res[1]); +} else { + window.__isDebug = false; + store.remove('debug'); +} + +//重要,用于矫正画布执行new Function的window对象上下文 +window.__newFunc = funContext => { + return new Function(funContext); +}; + +//关闭浏览器前提醒,只有产生过交互才会生效 +window.onbeforeunload = function(e) { + e = e || window.event; + // 本地调试不生效 + if (location.href.indexOf('localhost') > 0) return; + var msg = '您确定要离开此页面吗?'; + e.cancelBubble = true; + e.returnValue = msg; + if (e.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + return msg; +}; + +let instance: Editor; + +const debug = Debug('editor'); +EventEmitter.defaultMaxListeners = 100; + +export interface HooksFuncs { + [idx: number]: (msg: string, handler: (...args) => void) => void; +} + +export default class Editor extends EventEmitter { + static getInstance = (config: EditorConfig, components: PluginComponents, utils?: Utils): Editor => { + if (!instance) { + instance = new Editor(config, components, utils); + } + return instance; + }; + + private hooksFuncs: HooksFuncs; + + public pluginStatus: PluginStatus; + public plugins: PluginComponents; + public locale: LocaleType; + + public emit: (msg: string, ...args) => void; + public on: (msg: string, handler: (...args) => void) => void; + public once: (msg: string, handler: (...args) => void) => void; + public off: (msg: string, handler: (...args) => void) => void; + + constructor(public config: EditorConfig, public components: PluginComponents, public utils?: Utils) { + super(); + instance = this; + this.init(); + } + + init(): Promise { + const { hooks, shortCuts = [], lifeCycles } = this.config || {}; + this.locale = store.get('lowcode-editor-locale') || 'zh-CN'; + // this.messages = this.messagesSet[this.locale]; + // this.i18n = generateI18n(this.locale, this.messages); + this.pluginStatus = this.initPluginStatus(); + this.initHooks(hooks || []); + + this.emit('editor.beforeInit'); + const init = (lifeCycles && lifeCycles.init) || (() => {}); + // 用户可以通过设置extensions.init自定义初始化流程; + return transformToPromise(init(this)) + .then(() => { + // 注册快捷键 + registShortCuts(shortCuts, this); + this.emit('editor.afterInit'); + return true; + }) + .catch(err => { + console.error(err); + }); + } + + destroy() { + debug('destroy'); + try { + const { hooks = [], shortCuts = [], lifeCycles = {} } = this.config; + unRegistShortCuts(shortCuts); + this.destroyHooks(hooks); + lifeCycles.destroy && lifeCycles.destroy(this); + } catch (err) { + console.warn(err); + return; + } + } + + get(key: string): any { + return this[key]; + } + + set(key: string | object, val: any): void { + if (typeof key === 'string') { + if (['init', 'destroy', 'get', 'set', 'batchOn', 'batchOff', 'batchOnce'].includes(key)) { + console.error('init, destroy, get, set, batchOn, batchOff, batchOnce is private attribute'); + return; + } + this[key] = val; + } else if (typeof key === 'object') { + Object.keys(key).forEach(item => { + this[item] = key[item]; + }); + } + } + + batchOn(events: Array, lisenter: (...args) => void): void { + if (!Array.isArray(events)) return; + events.forEach(event => this.on(event, lisenter)); + } + + batchOnce(events: Array, lisenter: (...args) => void): void { + if (!Array.isArray(events)) return; + events.forEach(event => this.once(event, lisenter)); + } + + batchOff(events: Array, lisenter: (...args) => void): void { + if (!Array.isArray(events)) return; + events.forEach(event => this.off(event, lisenter)); + } + + //销毁hooks中的消息监听 + private destroyHooks(hooks: HooksConfig = []) { + hooks.forEach((item, idx) => { + if (typeof this.hooksFuncs[idx] === 'function') { + this.off(item.message, this.hooksFuncs[idx]); + } + }); + delete this.hooksFuncs; + } + + //初始化hooks中的消息监听 + private initHooks(hooks: HooksConfig = []): void { + this.hooksFuncs = hooks.map(item => { + const func = (...args) => { + item.handler(this, ...args); + }; + this[item.type](item.message, func); + return func; + }); + } + + private initPluginStatus() { + const { plugins = {} } = this.config; + const pluginAreas = Object.keys(plugins); + const res = {}; + pluginAreas.forEach(area => { + (plugins[area] || []).forEach(plugin => { + if (plugin.type === 'Divider') return; + const { visible, disabled, marked } = plugin.props || {}; + res[plugin.pluginKey] = { + visible: typeof visible === 'boolean' ? visible : true, + disabled: typeof disabled === 'boolean' ? disabled : false, + marked: typeof marked === 'boolean' ? marked : false + }; + const pluginClass = this.components[plugin.pluginKey]; + // 判断如果编辑器插件有init静态方法,则在此执行init方法 + if (pluginClass && pluginClass.init) { + pluginClass.init(this); + } + }); + }); + return res; + } +} diff --git a/packages/editor/src/framework/index.ts b/packages/editor/src/framework/index.ts new file mode 100644 index 000000000..be8b55d0a --- /dev/null +++ b/packages/editor/src/framework/index.ts @@ -0,0 +1,10 @@ +import Editor from './editor'; +export { default as PluginFactory } from './pluginFactory'; +export { default as EditorContext } from './context'; + +import * as editorUtils from './utils'; +import * as editorDefinitions from './definitions'; +export default Editor; + +export const utils = editorUtils; +export const definitions = editorDefinitions; diff --git a/packages/editor/src/framework/pluginFactory.tsx b/packages/editor/src/framework/pluginFactory.tsx new file mode 100644 index 000000000..b3a83ee83 --- /dev/null +++ b/packages/editor/src/framework/pluginFactory.tsx @@ -0,0 +1,87 @@ +import React, { PureComponent, createRef } from 'react'; + +import EditorContext from './context'; +import Editor from './editor'; +import { isEmpty, generateI18n, transformToPromise, acceptsRef } from './utils'; +import { PluginConfig, I18nFunction } from './definitions'; +import Editor from './index'; + +export interface PluginProps { + editor: Editor; + config: PluginConfig; +} + +export interface InjectedPluginProps { + i18n?: I18nFunction; +} + +export default function pluginFactory( + Comp: React.ComponentType +): React.ComponentType { + class LowcodePlugin extends PureComponent { + static displayName = 'LowcodeEditorPlugin'; + static defaultProps = { + config: {} + }; + static contextType = EditorContext; + static init = Comp.init; + public ref = createRef(); + private editor: Editor; + private pluginKey: string; + private i18n: I18nFunction; + + constructor(props, context) { + super(props, context); + if (isEmpty(props.config) || !props.config.pluginKey) { + console.warn('lowcode editor plugin has wrong config'); + return; + } + const { locale, messages, editor } = props; + // 注册插件 + this.editor = editor; + this.i18n = generateI18n(locale, messages); + this.pluginKey = props.config.pluginKey; + editor.set('plugins', { + ...editor.plugins, + [this.pluginKey]: this + }); + } + + componentWillUnmount() { + // 销毁插件 + if (this.editor && this.editor.plugins) { + delete this.editor.plugins[this.pluginKey]; + } + } + + open = (): Promise => { + if (this.ref && this.ref.open && typeof this.ref.open === 'function') { + return transformToPromise(this.ref.open()); + } + return Promise.resolve(); + }; + + close = () => { + if (this.ref && this.ref.close && typeof this.ref.close === 'function') { + return transformToPromise(this.ref.close()); + } + return Promise.resolve(); + }; + + render() { + const { config } = this.props; + const props = { + i18n: this.i18n, + editor: this.editor, + config, + ...config.pluginProps + }; + if (acceptsRef(Comp)) { + props.ref = this.ref; + } + return ; + } + } + + return LowcodePlugin; +} diff --git a/packages/editor/src/framework/utils.ts b/packages/editor/src/framework/utils.ts new file mode 100644 index 000000000..d4a6e4061 --- /dev/null +++ b/packages/editor/src/framework/utils.ts @@ -0,0 +1,246 @@ +import IntlMessageFormat from 'intl-messageformat'; +import keymaster from 'keymaster'; +import { EditorConfig, LocaleType, I18nMessages, I18nFunction, ShortCutsConfig } from './definitions'; +import Editor from './editor'; + +import _pick from 'lodash/pick'; +import _deepEqual from 'lodash/isEqualWith'; +import _clone from 'lodash/cloneDeep'; +import _isEmpty from 'lodash/isEmpty'; +import _throttle from 'lodash/throttle'; +import _debounce from 'lodash/debounce'; + +export const pick = _pick; +export const deepEqual = _deepEqual; +export const clone = _clone; +export const isEmpty = _isEmpty; +export const throttle = _throttle; +export const debounce = _debounce; + +import _serialize from 'serialize-javascript'; +export const serialize = _serialize; + +const ENV = { + TBE: 'TBE', + WEBIDE: 'WEB-IDE', + VSCODE: 'VSCODE', + WEB: 'WEB' +}; + +export interface IDEMessageParams { + action: string; + data: { + logKey: string; + gmKey: string; + goKey: string; + }; +} + +export interface Window { + sendIDEMessage: (IDEMessageParams) => void; + goldlog: { + record: (logKey: string, gmKey: string, goKey: string, method: 'GET' | 'POST') => void; + }; + parent: Window; + is_theia: boolean; + vscode: boolean; +} + +/** + * 用于构造国际化字符串处理函数 + */ +export function generateI18n(locale: LocaleType = 'zh-CN', messages: I18nMessages = {}): I18nFunction { + return (key, values = {}) => { + if (!messages || !messages[key]) return ''; + const formater = new IntlMessageFormat(messages[key], locale); + return formater.format(values); + }; +} + +/** + * 序列化参数 + */ +export function serializeParams(obj: object): string { + if (typeof obj !== 'object') return ''; + const res: Array = []; + Object.entries(obj).forEach(([key, val]) => { + if (val === null || val === undefined || val === '') return; + if (typeof val === 'object') { + res.push(`${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(val))}`); + } else { + res.push(`${encodeURIComponent(key)}=${encodeURIComponent(val)}`); + } + }); + return res.join('&'); +} + +/** + * 黄金令箭埋点 + * @param {String} gmKey 为黄金令箭业务类型 + * @param {Object} params 参数 + * @param {String} logKey 属性串 + */ +export function goldlog(gmKey: string, params: object = {}, logKey: string = 'other'): void { + const global = window as Window; + const sendIDEMessage = global.sendIDEMessage || global.parent.sendIDEMessage; + const goKey = serializeParams({ + env: getEnv(), + ...params + }); + if (sendIDEMessage) { + sendIDEMessage({ + action: 'goldlog', + data: { + logKey: `/iceluna.core.${logKey}`, + gmKey, + goKey + } + }); + } + global.goldlog && global.goldlog.record(`/iceluna.core.${logKey}`, gmKey, goKey, 'POST'); +} + +/** + * 获取当前编辑器环境 + */ +export function getEnv(): string { + const userAgent = navigator.userAgent; + const isVscode = /Electron\//.test(userAgent); + if (isVscode) return ENV.VSCODE; + const isTheia = window.is_theia === true; + if (isTheia) return ENV.WEBIDE; + return ENV.WEB; +} + +// 注册快捷键 +export function registShortCuts(config: ShortCutsConfig, editor: Editor): void { + (config || []).forEach(item => { + keymaster(item.keyboard, ev => { + ev.preventDefault(); + item.handler(editor, ev, keymaster); + }); + }); +} + +// 取消注册快捷 +export function unRegistShortCuts(config: ShortCutsConfig): void { + (config || []).forEach(item => { + keymaster.unbind(item.keyboard); + }); + if (window.parent.vscode) { + keymaster.unbind('command+c'); + keymaster.unbind('command+v'); + } +} + +/** + * 将函数返回结果转成promise形式,如果函数有返回值则根据返回值的bool类型判断是reject还是resolve,若函数无返回值默认执行resolve + */ +export function transformToPromise(input: any): Promise<{}> { + if (input instanceof Promise) return input; + return new Promise((resolve, reject) => { + if (input || input === undefined) { + resolve(); + } else { + reject(); + } + }); +} + +/** + * 将数组类型转换为Map类型 + */ +interface MapOf { + [propName: string]: T; +} +export function transformArrayToMap(arr: Array, key: string, overwrite: boolean = true): MapOf { + if (isEmpty(arr) || !Array.isArray(arr)) return {}; + const res = {}; + arr.forEach(item => { + const curKey = item[key]; + if (item[key] === undefined) return; + if (res[curKey] && !overwrite) return; + res[curKey] = item; + }); + return res; +} + +/** + * 解析url的查询参数 + */ +interface Query { + [propName: string]: string; +} +export function parseSearch(search: string): Query { + if (!search || typeof search !== 'string') return {}; + const str = search.replace(/^\?/, ''); + let paramStr = str.split('&'); + let res = {}; + for (let i = 0; i < paramStr.length; i++) { + let regRes = paramStr[i].split('='); + if (regRes[0] && regRes[1]) { + res[regRes[0]] = decodeURIComponent(regRes[1]); + } + } + return res; +} + +export function comboEditorConfig(defaultConfig: EditorConfig = {}, customConfig: EditorConfig): EditorConfig { + const { skeleton, theme, plugins, hooks, shortCuts, lifeCycles, constants, utils, i18n } = customConfig || {}; + + if (skeleton && skeleton.handler && typeof skeleton.handler === 'function') { + return skeleton.handler({ + skeleton, + ...defaultConfig + }); + } + + const defaultShortCuts = transformArrayToMap(defaultConfig.shortCuts || [], 'keyboard'); + const customShortCuts = transformArrayToMap(shortCuts || [], 'keyboard'); + const localeList = ['zh-CN', 'zh-TW', 'en-US', 'ja-JP']; + const i18nConfig = {}; + localeList.forEach(key => { + i18nConfig[key] = { + ...(defaultConfig.i18n && defaultConfig.i18n[key]), + ...(i18n && i18n[key]) + }; + }); + return { + skeleton, + theme: { + ...defaultConfig.theme, + ...theme + }, + plugins: { + ...defaultConfig.plugins, + ...plugins + }, + hooks: [...(defaultConfig.hooks || []), ...(hooks || [])], + shortCuts: Object.values({ + ...defaultShortCuts, + ...customShortCuts + }), + lifeCycles: { + ...defaultConfig.lifeCycles, + ...lifeCycles + }, + constants: { + ...defaultConfig.constants, + ...constants + }, + utils: [...(defaultConfig.utils || []), ...(utils || [])], + i18n: i18nConfig + }; +} + +/** + * 判断当前组件是否能够设置ref + * @param {*} Comp 需要判断的组件 + */ +export function acceptsRef(Comp: React.ComponentType) { + const hasSymbol = typeof Symbol === 'function' && Symbol['for']; + const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol['for']('react.forward_ref') : 0xead0; + return ( + (Comp.$$typeof && Comp.$$typeof === REACT_FORWARD_REF_TYPE) || (Comp.prototype && Comp.prototype.isReactComponent) + ); +} diff --git a/packages/editor/src/global.scss b/packages/editor/src/global.scss new file mode 100644 index 000000000..4802a89d4 --- /dev/null +++ b/packages/editor/src/global.scss @@ -0,0 +1,7 @@ +body { + font-family: PingFangSC-Regular, Roboto, Helvetica Neue, Helvetica, Tahoma, Arial, PingFang SC-Light, Microsoft YaHei; + font-size: 12px; + * { + box-sizing: border-box; + } +} diff --git a/packages/editor/src/index.tsx b/packages/editor/src/index.tsx new file mode 100644 index 000000000..b0c5fee5a --- /dev/null +++ b/packages/editor/src/index.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +// import Skeleton from '@ali/lowcode-engine-skeleton'; +import { HashRouter as Router, Route } from 'react-router-dom'; +import Skeleton from './skeleton'; +import config from './config/skeleton'; +import components from './config/components'; +import utils from './config/utils'; +import constants from './config/constants'; +import messages from './config/locale'; + +import pkg from '../package.json'; +import './global.scss'; +import './config/theme.scss'; + +window.__pkg = pkg; + +const ICE_CONTAINER = document.getElementById('ice-container'); + +if (!ICE_CONTAINER) { + throw new Error('当前页面不存在
节点.'); +} + +ReactDOM.render( + + ( + + )} + /> + , + ICE_CONTAINER +); diff --git a/packages/editor/src/plugins/designer/index.scss b/packages/editor/src/plugins/designer/index.scss new file mode 100644 index 000000000..0d512ea9a --- /dev/null +++ b/packages/editor/src/plugins/designer/index.scss @@ -0,0 +1,5 @@ +.lowcode-plugin-designer { + background-color: #ffffff; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/packages/editor/src/plugins/designer/index.tsx b/packages/editor/src/plugins/designer/index.tsx new file mode 100644 index 000000000..b5b0eda41 --- /dev/null +++ b/packages/editor/src/plugins/designer/index.tsx @@ -0,0 +1,152 @@ +import React, { PureComponent } from 'react'; + +import Editor from '../../framework/index'; +import { PluginConfig } from '../../framework/definitions'; + +import Designer from '../../../../designer/src/designer/designer-view'; + +import './index.scss'; + +export interface PluginProps { + editor: Editor; + config: PluginConfig; +} + +const SCHEMA = { + "componentName": "Page", + "fileName": "test", + "dataSource": { + "list": [{ + "id": "getComponentsMap", + "isInit": true, + "type": "doServer", + "options": { + "method": "POST", + "params": { + "libVersionIds": "1" + }, + "uri": "getComponentsMap" + } + }] + }, + "state": { + "text": "outter" + }, + "props": { + "ref": "outterView", + "autoLoading": true + }, + "children": [{ + "componentName": "Form", + "props": { + "labelCol": 4, + "onSubmit": function onSubmit(value, error, field) { + //form内有htmlType="submit"的元素的时候会触发 + alert(JSON.stringify(value)) + }, + "style": {}, + "ref": "testForm" + }, + "children": [{ + "componentName": "FormItem", + "props": { + "label": "姓名:", + "name": "name", + "initValue": "李雷" + }, + "children": [{ + "componentName": "Input", + "props": { + "placeholder": "请输入", + "size": "medium", + "style": { + "width": 320 + } + } + }] + }, { + "componentName": "FormItem", + "props": { + "label": "年龄:", + "name": "age", + "initValue": "22" + }, + "children": [{ + "componentName": "NumberPicker", + "props": { + "size": "medium", + "type": "normal" + } + }] + }, { + "componentName": "FormItem", + "props": { + "label": "职业:", + "name": "profession" + }, + "children": [{ + "componentName": "Select", + "props": { + "dataSource": [{ + "label": "教师", + "value": "t" + }, { + "label": "医生", + "value": "d" + }, { + "label": "歌手", + "value": "s" + }] + } + }] + }, { + "componentName": "Div", + "props": { + "style": { + "textAlign": "center" + } + }, + "children": [{ + "componentName": "ButtonGroup", + "props": {}, + "children": [{ + "componentName": "Button", + "props": { + "type": "primary", + "style": { + "margin": "0 5px 0 5px" + }, + "htmlType": "submit" + }, + "children": "提交" + }, { + "componentName": "Button", + "props": { + "type": "normal", + "style": { + "margin": "0 5px 0 5px" + }, + "htmlType": "reset" + }, + "children": "重置" + }] + }] + }] + }] +} + +export default class DesignerPlugin extends PureComponent { + static displayName: 'LowcodePluginDesigner'; + + constructor(props) { + super(props); + } + + render() { + return ( +
+ + +
); + } +} diff --git a/packages/editor/src/plugins/logo/index.scss b/packages/editor/src/plugins/logo/index.scss new file mode 100644 index 000000000..e2701fbca --- /dev/null +++ b/packages/editor/src/plugins/logo/index.scss @@ -0,0 +1,9 @@ +.lowcode-plugin-logo { + padding: 8px 16px; + .logo { + width: 56px; + height: 32px; + background-size: contain; + background-position: center; + } +} diff --git a/packages/editor/src/plugins/logo/index.tsx b/packages/editor/src/plugins/logo/index.tsx new file mode 100644 index 000000000..c02bf7a71 --- /dev/null +++ b/packages/editor/src/plugins/logo/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import './index.scss'; +import Editor from '../../framework/index'; +import { PluginConfig } from '../../framework/definitions'; + +export interface PluginProps { + editor: Editor; + config: PluginConfig; + logo?: string; +} + +export default function(props: PluginProps) { + return ( +
+
+
+ ); +} diff --git a/packages/editor/src/plugins/undoRedo/index.scss b/packages/editor/src/plugins/undoRedo/index.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/editor/src/plugins/undoRedo/index.tsx b/packages/editor/src/plugins/undoRedo/index.tsx new file mode 100644 index 000000000..2b1a47460 --- /dev/null +++ b/packages/editor/src/plugins/undoRedo/index.tsx @@ -0,0 +1,22 @@ +import React, {useState} from 'react'; +import './index.scss'; +import Editor from '../../framework/index'; +import { PluginConfig } from '../../framework/definitions'; +import TopIcon from '../../skeleton/components/TopIcon/index'; + +export interface PluginProps { + editor: Editor; + config: PluginConfig; + logo?: string; +} + +export default function(props: PluginProps) { + const [backEnable, setBackEnable] = useState(true); + const [forwardEnable, setForwardEnable] = useState(true); + return ( +
+ + +
+ ); +} diff --git a/packages/editor/src/skeleton/components/LeftPlugin/index.scss b/packages/editor/src/skeleton/components/LeftPlugin/index.scss new file mode 100644 index 000000000..06b6ef63a --- /dev/null +++ b/packages/editor/src/skeleton/components/LeftPlugin/index.scss @@ -0,0 +1,59 @@ +.lowcode-left-plugin { + font-size: 16px; + text-align: center; + line-height: 36px; + height: 36px; + position: relative; + cursor: pointer; + transition: all 0.3s ease; + color: #777; + &.collapse { + height: 40px; + color: #8c8c8c; + border-bottom: 1px solid #bfbfbf; + } + &.locked { + color: red !important; + } + &.active { + color: #fff !important; + background-color: $color-brand1-9 !important; + &.disabled { + color: #fff; + background-color: $color-fill1-7; + } + } + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + &:hover { + background-color: $color-brand1-1; + color: $color-brand1-6; + &:before { + content: attr(data-tooltip); + display: block; + position: absolute; + left: 50px; + top: 5px; + line-height: 18px; + font-size: 12px; + white-space: nowrap; + padding: 6px 8px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: #fff; + z-index: 100; + } + &:after { + content: ''; + display: block; + position: absolute; + left: 40px; + top: 15px; + border: 5px solid transparent; + border-right-color: rgba(0, 0, 0, 0.75); + z-index: 100; + } + } +} diff --git a/packages/editor/src/skeleton/components/LeftPlugin/index.tsx b/packages/editor/src/skeleton/components/LeftPlugin/index.tsx new file mode 100644 index 000000000..6d807548c --- /dev/null +++ b/packages/editor/src/skeleton/components/LeftPlugin/index.tsx @@ -0,0 +1,203 @@ +import React, { PureComponent, Fragment } from 'react'; +import classNames from 'classnames'; +import { Balloon, Dialog, Icon, Badge } from '@alifd/next'; + +import './index.scss'; +import Editor from '../../../framework/editor'; +import { PluginConfig, PluginClass } from '../../../framework/definitions'; + +export interface LeftPluginProps { + active?: boolean; + config: PluginConfig; + disabled?: boolean; + editor: Editor; + locked?: boolean; + marked?: boolean; + onClick?: () => void; + pluginClass: PluginClass; +} + +export interface LeftPluginState { + dialogVisible: boolean; +} + +export default class LeftPlugin extends PureComponent { + static displayName = 'LowcodeLeftPlugin'; + + static defaultProps = { + active: false, + config: {}, + disabled: false, + marked: false, + locked: false, + onClick: () => {} + }; + + constructor(props, context) { + super(props, context); + this.state = { + dialogVisible: false + }; + } + + componentDidMount() { + const { config, editor } = this.props; + const pluginKey = config && config.pluginKey; + if (editor && pluginKey) { + editor.on(`${pluginKey}.dialog.show`, this.handleShow); + editor.on(`${pluginKey}.dialog.close`, this.handleClose); + } + } + + componentWillUnmount() { + const { config, editor } = this.props; + const pluginKey = config && config.pluginKey; + if (editor && pluginKey) { + editor.off(`${pluginKey}.dialog.show`, this.handleShow); + editor.off(`${pluginKey}.dialog.close`, this.handleClose); + } + } + + handleClose = () => { + const { config, editor } = this.props; + const pluginKey = config && config.pluginKey; + const plugin = editor.plugins && editor.plugins[pluginKey]; + if (plugin) { + plugin.close().then(() => { + this.setState({ + dialogVisible: false + }); + }); + } + }; + + handleOpen = () => { + // todo 对话框类型的插件初始时拿不到插件实例 + this.setState({ + dialogVisible: true + }); + }; + + handleShow = () => { + const { disabled, config, onClick, editor } = this.props; + const pluginKey = config && config.pluginKey; + if (disabled || !pluginKey) return; + //考虑到弹窗情况,延时发送消息 + setTimeout(() => editor.emit(`${pluginKey}.addon.activate`), 0); + this.handleOpen(); + onClick && onClick(); + }; + + renderIcon = clickCallback => { + const { active, disabled, marked, locked, onClick, config } = this.props; + const { pluginKey, props } = config || {}; + const { icon, title } = props || {}; + return ( +
{ + if (disabled) return; + //考虑到弹窗情况,延时发送消息 + clickCallback && clickCallback(); + onClick && onClick(); + }} + > + {marked ? ( + + + + ) : ( + + )} +
+ ); + }; + + render() { + const { marked, locked, active, disabled, config, editor, pluginClass: Comp } = this.props; + const { pluginKey, props, type, pluginProps } = config || {}; + const { onClick, title } = props || {}; + const { dialogVisible } = this.state; + if (!pluginKey || !type || !props) return null; + + const node = + (Comp && ( + { + onClick && onClick.call(null, editor); + }} + {...pluginProps} + /> + )) || + null; + + switch (type) { + case 'LinkIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, editor); + })} + + ); + case 'Icon': + return this.renderIcon(() => { + onClick && onClick.call(null, editor); + }); + case 'DialogIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, editor); + this.handleOpen(); + })} + { + editor.emit(`${pluginKey}.dialog.onOk`); + this.handleClose(); + }} + onCancel={this.handleClose} + onClose={this.handleClose} + title={title} + {...(props.dialogProps || {})} + visible={dialogVisible} + > + {node} + + + ); + case 'BalloonIcon': + return ( + { + onClick && onClick.call(null, editor); + })} + align="r" + triggerType={['click', 'hover']} + {...(props.balloonProps || {})} + > + {node} + + ); + case 'PanelIcon': + return this.renderIcon(() => { + onClick && onClick.call(null, editor); + this.handleOpen(); + }); + case 'Custom': + return marked ? {node} : node; + default: + return null; + } + } +} diff --git a/packages/editor/src/skeleton/components/Panel/index.scss b/packages/editor/src/skeleton/components/Panel/index.scss new file mode 100644 index 000000000..cd3211ab4 --- /dev/null +++ b/packages/editor/src/skeleton/components/Panel/index.scss @@ -0,0 +1,52 @@ +.lowcode-panel { + user-select: none; + overflow: hidden; + position: relative; + background: #ffffff; + transition: width 0.3s ease; + transform: translate3d(0, 0, 0); + height: 100%; + &.visible { + border-right: 1px solid #bfbfbf; + } + .drag-area { + display: none; + } + &.floatable { + position: absolute; + top: 0; + bottom: 0; + z-index: 999; + } + &.draggable { + .drag-area { + display: block; + width: 10px; + position: absolute; + top: 0; + bottom: 0; + cursor: col-resize; + z-index: 9999; + } + &.left { + .drag-area { + right: 0; + } + } + &.right { + .drag-area { + left: 0; + } + } + } + &.left { + &.floatable { + left: 44px; + } + } + &.right { + &.floatable { + right: 44px; + } + } +} diff --git a/packages/editor/src/skeleton/components/Panel/index.tsx b/packages/editor/src/skeleton/components/Panel/index.tsx new file mode 100644 index 000000000..a1b9a9a8e --- /dev/null +++ b/packages/editor/src/skeleton/components/Panel/index.tsx @@ -0,0 +1,60 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; + +import './index.scss'; + +export interface PanelProps { + align: 'left' | 'right'; + defaultWidth: number; + minWidth: number; + draggable: boolean; + floatable: boolean; + children: Plugin; + visible: boolean; +} + +export interface PanelState { + width: number; +} + +export default class Panel extends PureComponent { + static displayName = 'LowcodePanel'; + + static defaultProps = { + align: 'left', + defaultWidth: 240, + minWidth: 100, + draggable: true, + floatable: false, + visible: true + }; + + constructor(props) { + super(props); + + this.state = { + width: props.defaultWidth + }; + } + + render() { + const { align, draggable, floatable, visible } = this.props; + const { width } = this.state; + return ( +
+ {this.props.children} +
+
+ ); + } +} diff --git a/packages/editor/src/skeleton/components/TopIcon/index.scss b/packages/editor/src/skeleton/components/TopIcon/index.scss new file mode 100644 index 000000000..67622fb64 --- /dev/null +++ b/packages/editor/src/skeleton/components/TopIcon/index.scss @@ -0,0 +1,67 @@ +.next-btn.next-large.lowcode-top-btn { + width: 44px; + height: 44px; + padding: 0; + margin: 2px -2px; + text-align: center; + border-radius: 8px; + border: 1px solid transparent; + color: #777; + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + &.locked { + color: red !important; + } + &:hover { + background-color: $color-brand1-1; + color: $color-brand1-6; + &:before { + content: attr(data-tooltip); + display: block; + height: auto; + width: auto; + position: absolute; + left: 50%; + transform: translate(-50%, 0); + bottom: -35px; + line-height: 18px; + font-size: 12px; + white-space: nowrap; + padding: 6px 8px; + border-radius: 4px; + background: rgba(0, 0, 0, 0.75); + color: #fff; + z-index: 100; + } + &:after { + content: ''; + display: block; + position: absolute; + left: 50%; + transform: translate(-50%, 0); + bottom: -5px; + border: 5px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.75); + opacity: 1; + visibility: visible; + z-index: 100; + } + } + i.next-icon { + &:before { + font-size: 17px; + } + margin-right: 0; + line-height: 18px; + } + span { + display: block; + margin: 0px -5px 0; + line-height: 16px; + text-align: center; + font-size: 12px; + transform: scale(0.8); + } +} diff --git a/packages/editor/src/skeleton/components/TopIcon/index.tsx b/packages/editor/src/skeleton/components/TopIcon/index.tsx new file mode 100644 index 000000000..64d27a6d7 --- /dev/null +++ b/packages/editor/src/skeleton/components/TopIcon/index.tsx @@ -0,0 +1,60 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; +import { Icon, Button } from '@alifd/next'; + +import './index.scss'; + +export interface TopIconProps { + active?: boolean; + className?: string; + disabled?: boolean; + icon: string; + id?: string; + locked?: boolean; + marked?: boolean; + onClick?: () => void; + showTitle?: boolean; + style?: React.CSSProperties; + title?: string; +} + +export default class TopIcon extends PureComponent { + static displayName = 'LowcodeTopIcon'; + static defaultProps = { + active: false, + className: '', + disabled: false, + icon: '', + id: '', + locked: false, + onClick: () => {}, + showTitle: false, + style: {}, + title: '' + }; + + render() { + const { active, disabled, icon, locked, title, className, id, style, showTitle, onClick } = this.props; + return ( + + ); + } +} diff --git a/packages/editor/src/skeleton/components/TopPlugin/index.scss b/packages/editor/src/skeleton/components/TopPlugin/index.scss new file mode 100644 index 000000000..4bdd7b8d2 --- /dev/null +++ b/packages/editor/src/skeleton/components/TopPlugin/index.scss @@ -0,0 +1,2 @@ +.lowcode-top-addon { +} diff --git a/packages/editor/src/skeleton/components/TopPlugin/index.tsx b/packages/editor/src/skeleton/components/TopPlugin/index.tsx new file mode 100644 index 000000000..6c4f280d0 --- /dev/null +++ b/packages/editor/src/skeleton/components/TopPlugin/index.tsx @@ -0,0 +1,191 @@ +import React, { PureComponent, Fragment } from 'react'; + +import TopIcon from '../TopIcon'; +import { Balloon, Badge, Dialog } from '@alifd/next'; + +import './index.scss'; +import { PluginConfig, PluginClass } from '../../../framework/definitions'; +import Editor from '../../../framework/editor'; + +export interface TopPluginProps { + active?: boolean; + config: PluginConfig; + disabled?: boolean; + editor: Editor; + locked?: boolean; + marked?: boolean; + onClick?: () => void; + pluginClass: PluginClass; +} + +export interface TopPluginState { + dialogVisible: boolean; +} + +export default class TopPlugin extends PureComponent { + static displayName = 'LowcodeTopPlugin'; + + static defaultProps = { + active: false, + config: {}, + disabled: false, + marked: false, + locked: false, + onClick: () => {} + }; + + constructor(props, context) { + super(props, context); + this.state = { + dialogVisible: false + }; + } + + componentDidMount() { + const { config, editor } = this.props; + const pluginKey = config && config.pluginKey; + if (editor && pluginKey) { + editor.on(`${pluginKey}.dialog.show`, this.handleShow); + editor.on(`${pluginKey}.dialog.close`, this.handleClose); + } + } + + componentWillUnmount() { + const { config, editor } = this.props; + const pluginKey = config && config.pluginKey; + if (editor && pluginKey) { + editor.off(`${pluginKey}.dialog.show`, this.handleShow); + editor.off(`${pluginKey}.dialog.close`, this.handleClose); + } + } + + handleShow = () => { + const { disabled, config, onClick, editor } = this.props; + const pluginKey = config && config.pluginKey; + if (disabled || !pluginKey) return; + //考虑到弹窗情况,延时发送消息 + setTimeout(() => editor.emit(`${pluginKey}.addon.activate`), 0); + this.handleOpen(); + onClick && onClick(); + }; + + handleClose = () => { + const { config, editor } = this.props; + const pluginKey = config && config.pluginKey; + const plugin = editor.plugins && editor.plugins[pluginKey]; + if (plugin) { + plugin.close().then(() => { + this.setState({ + dialogVisible: false + }); + }); + } + }; + + handleOpen = () => { + // todo dialog类型的插件初始时拿不动插件实例 + this.setState({ + dialogVisible: true + }); + }; + + renderIcon = clickCallback => { + const { active, disabled, marked, locked, config, onClick, editor } = this.props; + const { pluginKey, props } = config || {}; + const { icon, title } = props || {}; + const node = ( + { + if (disabled) return; + //考虑到弹窗情况,延时发送消息 + setTimeout(() => editor.emit(`${pluginKey}.addon.activate`), 0); + clickCallback && clickCallback(); + onClick && onClick(); + }} + /> + ); + return marked ? {node} : node; + }; + + render() { + const { active, marked, locked, disabled, config, editor, pluginClass: Comp } = this.props; + const { pluginKey, pluginProps, props, type } = config || {}; + const { onClick, title } = props || {}; + const { dialogVisible } = this.state; + if (!pluginKey || !type) return null; + const node = + (Comp && ( + { + onClick && onClick.call(null, editor); + }} + {...pluginProps} + /> + )) || + null; + + switch (type) { + case 'LinkIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, editor); + })} + + ); + case 'Icon': + return this.renderIcon(() => { + onClick && onClick.call(null, editor); + }); + case 'DialogIcon': + return ( + + {this.renderIcon(() => { + onClick && onClick.call(null, editor); + this.handleOpen(); + })} + { + editor.emit(`${pluginKey}.dialog.onOk`); + this.handleClose(); + }} + onCancel={this.handleClose} + onClose={this.handleClose} + title={title} + {...props.dialogProps} + visible={dialogVisible} + > + {node} + + + ); + case 'BalloonIcon': + return ( + { + onClick && onClick.call(null, editor); + })} + triggerType={['click', 'hover']} + {...props.balloonProps} + > + {node} + + ); + case 'Custom': + return marked ? {node} : node; + default: + return null; + } + } +} diff --git a/packages/editor/src/skeleton/config/skeleton.ts b/packages/editor/src/skeleton/config/skeleton.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/skeleton/config/skeleton.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/skeleton/config/utils.ts b/packages/editor/src/skeleton/config/utils.ts new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/skeleton/config/utils.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/skeleton/global.scss b/packages/editor/src/skeleton/global.scss new file mode 100644 index 000000000..514f5b463 --- /dev/null +++ b/packages/editor/src/skeleton/global.scss @@ -0,0 +1,32 @@ +body { + font-family: PingFangSC-Regular, Roboto, Helvetica Neue, Helvetica, Tahoma, Arial, PingFang SC-Light, Microsoft YaHei; + font-size: 12px; + padding: 0; + margin: 0; + * { + box-sizing: border-box; + } +} +.next-loading { + .next-loading-wrap { + height: 100%; + } +} +.lowcode-editor { + .lowcode-main-content { + position: absolute; + top: 48px; + left: 0; + right: 0; + bottom: 0; + display: flex; + background-color: #d8d8d8; + } + .lowcode-center-area { + flex: 1; + display: flex; + flex-direction: column; + padding: 10px; + overflow: auto; + } +} diff --git a/packages/editor/src/skeleton/index.tsx b/packages/editor/src/skeleton/index.tsx new file mode 100644 index 000000000..b721c36bb --- /dev/null +++ b/packages/editor/src/skeleton/index.tsx @@ -0,0 +1,131 @@ +import React, { PureComponent } from 'react'; + +import { HashRouter as Router, Route } from 'react-router-dom'; +import { Loading, ConfigProvider } from '@alifd/next'; + +import Editor from '../framework/editor'; +import { EditorConfig, Utils, PluginComponents } from '../framework/definitions'; +import { comboEditorConfig, parseSearch } from '../framework/utils'; + +import defaultConfig from './config/skeleton'; +import skeletonUtils from './config/utils'; + +import TopArea from './layouts/TopArea'; +import LeftArea from './layouts/LeftArea'; +import CenterArea from './layouts/CenterArea'; +import RightArea from './layouts/RightArea'; + +import './global.scss'; + +let renderIdx = 0; + +export interface SkeletonProps { + components: PluginComponents; + config: EditorConfig; + history: object; + location: object; + match: object; + utils: Utils; +} + +export interface SkeletonState { + initReady: boolean; + skeletonKey: string; + __hasError?: boolean; +} + +export default class Skeleton extends PureComponent { + static displayName = 'LowcodeEditorSkeleton'; + + static getDerivedStateFromError() { + return { + __hasError: true + }; + } + + private editor: Editor; + + constructor(props) { + super(props); + + this.state = { + initReady: false, + skeletonKey: `skeleton${renderIdx}` + }; + + this.init(); + } + + componentWillUnmount() { + this.editor && this.editor.destroy(); + } + + componentDidCatch(err) { + console.error(err); + } + + init = (isReset: boolean = false): void => { + if (this.editor) { + this.editor.destroy(); + } + const { utils, config, components } = this.props; + const editor = (this.editor = new Editor(comboEditorConfig(defaultConfig, config), components, { + ...skeletonUtils, + ...utils + })); + window.__ctx = { + editor, + appHelper: editor + }; + editor.once('editor.reset', () => { + this.setState({ + initReady: false + }); + editor.emit('editor.beforeReset'); + this.init(true); + }); + + this.editor.init().then(() => { + this.setState( + { + initReady: true, + //刷新IDE时生成新的skeletonKey保证插件生命周期重新执行 + skeletonKey: isReset ? `skeleton${++renderIdx}` : this.state.skeletonKey + }, + () => { + editor.emit('editor.ready'); + isReset && editor.emit('ide.afterReset'); + } + ); + }); + }; + + render() { + const { initReady, skeletonKey, __hasError } = this.state; + const { location, history, match } = this.props; + if (__hasError || !this.editor) { + return 'error'; + } + + location.query = parseSearch(location.search); + this.editor.set('location', location); + this.editor.set('history', history); + this.editor.set('match', match); + + return ( + + +
+ +
+ + + + +
+
+
+
+ ); + } +} diff --git a/packages/editor/src/skeleton/layouts/CenterArea/index.scss b/packages/editor/src/skeleton/layouts/CenterArea/index.scss new file mode 100644 index 000000000..b2584ed2b --- /dev/null +++ b/packages/editor/src/skeleton/layouts/CenterArea/index.scss @@ -0,0 +1,3 @@ +.lowcode-center-area { + padding: 12px; +} diff --git a/packages/editor/src/skeleton/layouts/CenterArea/index.tsx b/packages/editor/src/skeleton/layouts/CenterArea/index.tsx new file mode 100644 index 000000000..1389a00e6 --- /dev/null +++ b/packages/editor/src/skeleton/layouts/CenterArea/index.tsx @@ -0,0 +1,48 @@ +import React, { PureComponent } from 'react'; + +import Editor from '../../../framework/editor'; +import './index.scss'; +import AreaManager from '../../../framework/areaManager'; + +export interface CenterAreaProps { + editor: Editor; +} + +export default class CenterArea extends PureComponent { + static displayName = 'LowcodeCenterArea'; + + private editor: Editor; + private areaManager: AreaManager; + + constructor(props) { + super(props); + this.editor = props.editor; + this.areaManager = new AreaManager(this.editor, 'centerArea'); + } + + componentDidMount() { + this.editor.on('skeleton.update', this.handleSkeletonUpdate); + } + componentWillUnmount() { + this.editor.off('skeleton.update', this.handleSkeletonUpdate); + } + + handleSkeletonUpdate = (): void => { + // 当前区域插件状态改变是更新区域 + if (this.areaManager.isPluginStatusUpdate()) { + this.forceUpdate(); + } + }; + + render() { + const visiblePluginList = this.areaManager.getVisiblePluginList(); + return ( +
+ {visiblePluginList.map(item => { + const Comp = this.editor.components[item.pluginKey]; + return ; + })} +
+ ); + } +} diff --git a/packages/editor/src/skeleton/layouts/LeftArea/index.scss b/packages/editor/src/skeleton/layouts/LeftArea/index.scss new file mode 100644 index 000000000..dac1b6b0a --- /dev/null +++ b/packages/editor/src/skeleton/layouts/LeftArea/index.scss @@ -0,0 +1,21 @@ +.lowcode-left-area-nav { + width: 48px; + height: 100%; + background: #ffffff; + border-right: 1px solid #e8ebee; + position: relative; + .top-area { + position: absolute; + top: 0; + width: 100%; + background: #ffffff; + max-height: 100%; + } + .bottom-area { + position: absolute; + bottom: 20px; + width: 100%; + background: #ffffff; + max-height: calc(100% - 20px); + } +} diff --git a/packages/editor/src/skeleton/layouts/LeftArea/index.tsx b/packages/editor/src/skeleton/layouts/LeftArea/index.tsx new file mode 100644 index 000000000..7f4684546 --- /dev/null +++ b/packages/editor/src/skeleton/layouts/LeftArea/index.tsx @@ -0,0 +1,7 @@ +import Nav from './nav'; +import Panel from './panel'; + +export default { + Nav, + Panel +}; diff --git a/packages/editor/src/skeleton/layouts/LeftArea/nav.tsx b/packages/editor/src/skeleton/layouts/LeftArea/nav.tsx new file mode 100644 index 000000000..a5ed5cd8f --- /dev/null +++ b/packages/editor/src/skeleton/layouts/LeftArea/nav.tsx @@ -0,0 +1,172 @@ +import React, { PureComponent } from 'react'; +import LeftPlugin from '../../components/LeftPlugin'; +import './index.scss'; +import Editor from '../../../framework/editor'; +import { PluginConfig } from '../../../framework/definitions'; +import AreaManager from '../../../framework/areaManager'; + +export interface LeftAreaNavProps { + editor: Editor; +} + +export interface LeftAreaNavState { + activeKey: string; +} + +export default class LeftAreaNav extends PureComponent { + static displayName = 'LowcodeLeftAreaNav'; + + private editor: Editor; + private areaManager: AreaManager; + private cacheActiveKey: string; + + constructor(props) { + super(props); + this.editor = props.editor; + this.areaManager = new AreaManager(this.editor, 'leftArea'); + + this.state = { + activeKey: 'none' + }; + this.cacheActiveKey = 'none'; + } + + componentDidMount() { + this.editor.on('skeleton.update', this.handleSkeletonUpdate); + this.editor.on('leftNav.change', this.handlePluginChange); + const visiblePanelPluginList = this.areaManager.getVisiblePluginList().filter(item => item.type === 'IconPanel'); + const defaultKey = (visiblePanelPluginList[0] && visiblePanelPluginList[0].pluginKey) || 'componentAttr'; + this.handlePluginChange(defaultKey); + } + componentWillUnmount() { + this.editor.off('skeleton.update', this.handleSkeletonUpdate); + this.editor.off('leftNav.change', this.handlePluginChange); + } + + handleSkeletonUpdate = (): void => { + // 当前区域插件状态改变是更新区域 + if (this.areaManager.isPluginStatusUpdate()) { + this.forceUpdate(); + } + }; + + handlePluginChange = (key: string): void => { + const { activeKey } = this.state; + const plugins = this.editor.plugins; + const prePlugin = plugins[activeKey]; + const nextPlugin = plugins[key]; + if (activeKey === 'none') { + if (nextPlugin) { + nextPlugin.open().then(() => { + this.updateActiveKey(key); + }); + } + } else if (activeKey === key) { + if (prePlugin) { + prePlugin.close().then(() => { + this.updateActiveKey('none'); + }); + } + } else { + // 先关后开 + if (prePlugin) { + prePlugin.close().then(() => { + if (nextPlugin) { + nextPlugin.open().then(() => { + this.updateActiveKey(key); + }); + } + }); + } + } + }; + + handleCollapseClick = (): void => { + const { activeKey } = this.state; + if (activeKey === 'none') { + const plugin = this.editor.plugins[this.cacheActiveKey]; + if (plugin) { + plugin.open().then(() => { + this.updateActiveKey(this.cacheActiveKey); + }); + } + } else { + const plugin = this.editor.plugins[activeKey]; + if (plugin) { + plugin.close().then(() => { + this.updateActiveKey('none'); + }); + } + } + }; + + handlePluginClick = (item: PluginConfig): void => { + if (item.type === 'PanelIcon') { + this.handlePluginChange(item.pluginKey); + } + }; + + updateActiveKey = (key: string): void => { + if (key === 'none') { + this.cacheActiveKey = this.state.activeKey; + } + this.editor.set('leftNav', key); + this.setState({ activeKey: key }); + this.editor.emit('leftPanel.show', key); + }; + + renderPluginList = (list: Array = []): Array => { + const { activeKey } = this.state; + return list.map((item, idx) => { + const pluginStatus = this.editor.pluginStatus[item.pluginKey]; + return ( + this.handlePluginClick(item)} + active={activeKey === item.pluginKey} + {...pluginStatus} + /> + ); + }); + }; + + render() { + const { activeKey } = this.state; + const topList: Array = []; + const bottomList: Array = []; + const visiblePluginList = this.areaManager.getVisiblePluginList(); + visiblePluginList.forEach(item => { + const align = item.props && item.props.align === 'bottom' ? 'bottom' : 'top'; + if (align === 'bottom') { + bottomList.push(item); + } else { + topList.push(item); + } + }); + + return ( +
+
{this.renderPluginList(bottomList)}
+
+ + {this.renderPluginList(topList)} +
+
+ ); + } +} diff --git a/packages/editor/src/skeleton/layouts/LeftArea/panel.tsx b/packages/editor/src/skeleton/layouts/LeftArea/panel.tsx new file mode 100644 index 000000000..8c03247fd --- /dev/null +++ b/packages/editor/src/skeleton/layouts/LeftArea/panel.tsx @@ -0,0 +1,70 @@ +import React, { PureComponent, Fragment } from 'react'; +import Panel from '../../components/Panel'; +import './index.scss'; +import Editor from '../../../framework/editor'; +import AreaManager from '../../../framework/areaManager'; + +export interface LeftAreaPanelProps { + editor: Editor; +} + +export interface LeftAreaPanelState { + activeKey: string; +} + +export default class LeftAreaPanel extends PureComponent { + static displayName = 'LowcodeLeftAreaPanel'; + + private editor: Editor; + private areaManager: AreaManager; + + constructor(props) { + super(props); + this.editor = props.editor; + this.areaManager = new AreaManager(this.editor, 'leftArea'); + + this.state = { + activeKey: 'none' + }; + } + + componentDidMount() { + this.editor.on('skeleton.update', this.handleSkeletonUpdate); + this.editor.on('leftPanel.show', this.handlePluginChange); + } + componentWillUnmount() { + this.editor.off('skeleton.update', this.handleSkeletonUpdate); + this.editor.off('leftPanel.show', this.handlePluginChange); + } + + handleSkeletonUpdate = (): void => { + // 当前区域插件状态改变是更新区域 + if (this.areaManager.isPluginStatusUpdate('PanelIcon')) { + this.forceUpdate(); + } + }; + + handlePluginChange = (key: string): void => { + this.setState({ + activeKey: key + }); + }; + + render() { + const { activeKey } = this.state; + const list = this.areaManager.getVisiblePluginList('PanelIcon'); + + return ( + + {list.map((item, idx) => { + const Comp = this.editor.components[item.pluginKey]; + return ( + + + + ); + })} + + ); + } +} diff --git a/packages/editor/src/skeleton/layouts/RightArea/index.scss b/packages/editor/src/skeleton/layouts/RightArea/index.scss new file mode 100644 index 000000000..fd05f5644 --- /dev/null +++ b/packages/editor/src/skeleton/layouts/RightArea/index.scss @@ -0,0 +1,52 @@ +.lowcode-right-area { + width: 300px; + height: 100%; + background-color: #ffffff; + border-left: 1px solid #e8ebee; + .right-plugin-title { + &.locked { + color: red !important; + } + &.active { + color: $color-brand1-9 !important; + } + &.disabled { + cursor: not-allowed; + color: $color-text1-1; + } + } + + //tab定义 + .next-tabs-wrapped.right-tabs { + display: flex; + flex-direction: column; + margin-top: -1px; + .next-tabs-bar { + z-index: 1; + } + .next-tabs-nav { + display: block; + .next-tabs-tab { + &:first-child { + border-left: none; + } + font-size: 14px; + text-align: center; + border-right: none !important; + margin-right: 0 !important; + width: 25%; + &.active { + background: none; + border-bottom-color: #f7f7f7 !important; + } + } + } + } + .next-tabs-content { + flex: 1; + .next-tabs-tabpane.active { + height: 100%; + overflow-y: auto; + } + } +} diff --git a/packages/editor/src/skeleton/layouts/RightArea/index.tsx b/packages/editor/src/skeleton/layouts/RightArea/index.tsx new file mode 100644 index 000000000..5b51d02fb --- /dev/null +++ b/packages/editor/src/skeleton/layouts/RightArea/index.tsx @@ -0,0 +1,159 @@ +import React, { PureComponent } from 'react'; +import { Tab, Badge, Icon } from '@alifd/next'; +import './index.scss'; +import Editor from '../../../framework/editor'; +import AreaManager from '../../../framework/areaManager'; +import { PluginConfig } from '../../../framework/definitions'; + +export interface RightAreaProps { + editor: Editor; +} + +export interface RightAreaState { + activeKey: string; +} + +export default class RightArea extends PureComponent { + static displayName = 'LowcodeRightArea'; + + private editor: Editor; + private areaManager: AreaManager; + + constructor(props) { + super(props); + this.editor = props.editor; + this.areaManager = new AreaManager(this.editor, 'rightArea'); + this.state = { + activeKey: '' + }; + } + + componentDidMount() { + this.editor.on('skeleton.update', this.handleSkeletonUpdate); + this.editor.on('rightNav.change', this.handlePluginChange); + const visiblePluginList = this.areaManager.getVisiblePluginList(); + const defaultKey = (visiblePluginList[0] && visiblePluginList[0].pluginKey) || 'componentAttr'; + this.handlePluginChange(defaultKey, true); + } + componentWillUnmount() { + this.editor.off('skeleton.update', this.handleSkeletonUpdate); + this.editor.off('rightNav.change', this.handlePluginChange); + } + + handleSkeletonUpdate = (): void => { + // 当前区域插件状态改变是更新区域 + if (this.areaManager.isPluginStatusUpdate()) { + const pluginStatus = this.editor.pluginStatus; + const activeKey = this.state.activeKey; + if (pluginStatus[activeKey] && pluginStatus[activeKey].visible) { + this.forceUpdate(); + } else { + const currentPlugin = this.editor.plugins[activeKey]; + if (currentPlugin) { + currentPlugin.close().then(() => { + this.setState( + { + activeKey: '' + }, + () => { + const visiblePluginList = this.areaManager.getVisiblePluginList(); + const firstPlugin = visiblePluginList && visiblePluginList[0]; + if (firstPlugin) { + this.handlePluginChange(firstPlugin.pluginKey); + } + } + ); + }); + } + } + } + }; + + handlePluginChange = (key: string, isinit?: boolean): void => { + const activeKey = this.state.activeKey; + const plugins = this.editor.plugins || {}; + const openPlugin = () => { + if (!plugins[key]) { + console.error(`plugin ${key} has not regist in the editor`); + return; + } + plugins[key].open().then(() => { + this.editor.set('rightNav', key); + this.setState({ + activeKey: key + }); + }); + }; + if (key === activeKey && !isinit) return; + if (activeKey && plugins[activeKey]) { + plugins[activeKey].close().then(() => { + openPlugin(); + }); + } else { + openPlugin(); + } + }; + + renderTabTitle = (config: PluginConfig): React.ReactElement => { + const { icon, title } = config.props || {}; + const pluginStatus = this.editor.pluginStatus[config.pluginKey]; + const { marked, disabled, locked } = pluginStatus; + const active = this.state.activeKey === config.pluginKey; + + const renderTitle = (): React.ReactElement => ( +
+ {!!icon && ( + + )} + {title} +
+ ); + if (marked) { + return {renderTitle()}; + } + return renderTitle(); + }; + + render() { + const visiblePluginList = this.areaManager.getVisiblePluginList(); + return ( +
+ + {visiblePluginList.map((item, idx) => { + const Comp = this.editor.components[item.pluginKey]; + return ( + + + + ); + })} + +
+ ); + } +} diff --git a/packages/editor/src/skeleton/layouts/TopArea/index.scss b/packages/editor/src/skeleton/layouts/TopArea/index.scss new file mode 100644 index 000000000..98af961da --- /dev/null +++ b/packages/editor/src/skeleton/layouts/TopArea/index.scss @@ -0,0 +1,26 @@ +.lowcode-top-area { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 48px; + background-color: #ffffff; + border-bottom: 1px solid #e8ebee; + user-select: none; + .divider { + max-width: 0; + margin: 8px 12px; + height: 30px; + border-right: 1px solid rgba(191, 191, 191, 0.3); + } + .next-col { + text-align: center; + } + .right-area { + position: absolute; + right: 12px; + top: 0; + height: 100%; + background: #ffffff; + } +} diff --git a/packages/editor/src/skeleton/layouts/TopArea/index.tsx b/packages/editor/src/skeleton/layouts/TopArea/index.tsx new file mode 100644 index 000000000..df8828a9d --- /dev/null +++ b/packages/editor/src/skeleton/layouts/TopArea/index.tsx @@ -0,0 +1,89 @@ +import React, { PureComponent } from 'react'; +import { Grid } from '@alifd/next'; +import TopPlugin from '../../components/TopPlugin'; +import './index.scss'; +import Editor from '../../../framework/index'; +import { PluginConfig } from '../../../framework/definitions'; +import AreaManager from '../../../framework/areaManager'; + +const { Row, Col } = Grid; + +export interface TopAreaProps { + editor: Editor; +} + +export default class TopArea extends PureComponent { + static displayName = 'LowcodeTopArea'; + + private areaManager: AreaManager; + private editor: Editor; + + constructor(props) { + super(props); + this.editor = props.editor; + this.areaManager = new AreaManager(props.editor, 'topArea'); + } + + componentDidMount() { + this.editor.on('skeleton.update', this.handleSkeletonUpdate); + } + componentWillUnmount() { + this.editor.off('skeleton.update', this.handleSkeletonUpdate); + } + + handleSkeletonUpdate = (): void => { + // 当前区域插件状态改变是更新区域 + if (this.areaManager.isPluginStatusUpdate()) { + this.forceUpdate(); + } + }; + + renderPluginList = (list: Array = []): Array => { + return list.map((item, idx) => { + const isDivider = item.type === 'Divider'; + return ( + + {!isDivider && ( + + )} + + ); + }); + }; + + render() { + const leftList: Array = []; + const rightList: Array = []; + const visiblePluginList = this.areaManager.getVisiblePluginList(); + visiblePluginList.forEach(item => { + const align = item.props && item.props.align === 'right' ? 'right' : 'left'; + // 分隔符不允许相邻 + if (item.type === 'Divider') { + const currList = align === 'right' ? rightList : leftList; + if (currList.length === 0 || currList[currList.length - 1].type === 'Divider') return; + } + if (align === 'right') { + rightList.push(item); + } else { + leftList.push(item); + } + }); + return ( +
+
+ {this.renderPluginList(leftList)} +
+
+ {this.renderPluginList(rightList)} +
+
+ ); + } +} diff --git a/packages/editor/src/skeleton/locale/en-US.js b/packages/editor/src/skeleton/locale/en-US.js new file mode 100644 index 000000000..936701e33 --- /dev/null +++ b/packages/editor/src/skeleton/locale/en-US.js @@ -0,0 +1,10 @@ +export default { + loading: 'loading...', + rejectRedirect: 'Redirect is not allowed', + expand: 'Unfold', + fold: 'Fold', + pageNotExist: 'The current Page not exist', + enterFromAppCenter: 'Please enter from the app center', + noPermission: 'Sorry, you do not have the develop permission', + getPermission: 'Please connect the app owners {owners} to get the permission' +}; diff --git a/packages/editor/src/skeleton/locale/ja-JP.js b/packages/editor/src/skeleton/locale/ja-JP.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/skeleton/locale/ja-JP.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/skeleton/locale/zh-CN.js b/packages/editor/src/skeleton/locale/zh-CN.js new file mode 100644 index 000000000..efe4ea898 --- /dev/null +++ b/packages/editor/src/skeleton/locale/zh-CN.js @@ -0,0 +1,10 @@ +export default { + loading: '加载中...', + rejectRedirect: '开发中,已阻止发生跳转', + expand: '展开', + fold: '收起', + pageNotExist: '当前访问地址不存在', + enterFromAppCenter: '请从应用中心入口重新进入', + noPermission: '抱歉,您暂无开发权限', + getPermission: '请移步应用中心申请开发权限, 或联系 {owners} 开通权限' +}; diff --git a/packages/editor/src/skeleton/locale/zh-TW.js b/packages/editor/src/skeleton/locale/zh-TW.js new file mode 100644 index 000000000..ff8b4c563 --- /dev/null +++ b/packages/editor/src/skeleton/locale/zh-TW.js @@ -0,0 +1 @@ +export default {}; diff --git a/packages/editor/src/skeleton/skeleton.tsx b/packages/editor/src/skeleton/skeleton.tsx new file mode 100644 index 000000000..7fdec576f --- /dev/null +++ b/packages/editor/src/skeleton/skeleton.tsx @@ -0,0 +1,138 @@ +import React, { PureComponent } from 'react'; + +import { HashRouter as Router, Route } from 'react-router-dom'; +import { Loading, ConfigProvider } from '@alifd/next'; + +import Editor from '../framework/editor'; +import { EditorConfig, Utils, PluginComponents } from '../framework/definitions'; +import { comboEditorConfig, parseSearch } from '../framework/utils'; + +import defaultConfig from './config/skeleton'; +import skeletonUtils from './config/utils'; + +import TopArea from './layouts/TopArea'; +import LeftArea from './layouts/LeftArea'; +import CenterArea from './layouts/CenterArea'; +import RightArea from './layouts/RightArea'; + +import './global.scss'; + +let renderIdx = 0; + +export interface SkeletonProps { + components: PluginComponents; + config: EditorConfig; + utils: Utils; +} + +export interface SkeletonState { + initReady: boolean; + skeletonKey: string; + __hasError?: boolean; +} + +export default class Skeleton extends PureComponent { + static displayName = 'LowcodeEditorSkeleton'; + + static getDerivedStateFromError() { + return { + __hasError: true + }; + } + + private editor: Editor; + + constructor(props) { + super(props); + + this.state = { + initReady: false, + skeletonKey: `skeleton${renderIdx}` + }; + + this.init(); + } + + componentWillUnmount() { + this.editor && this.editor.destroy(); + } + + componentDidCatch(err) { + console.error(err); + } + + init = (isReset: boolean = false): void => { + if (this.editor) { + this.editor.destroy(); + } + const { utils, config, components } = this.props; + debugger; + const editor = (this.editor = new Editor(comboEditorConfig(defaultConfig, config), components, { + ...skeletonUtils, + ...utils + })); + window.__ctx = { + editor, + appHelper: editor + }; + editor.once('editor.reset', () => { + this.setState({ + initReady: false + }); + editor.emit('editor.beforeReset'); + this.init(true); + }); + + this.editor.init().then(() => { + this.setState( + { + initReady: true, + //刷新IDE时生成新的skeletonKey保证插件生命周期重新执行 + skeletonKey: isReset ? `skeleton${++renderIdx}` : this.state.skeletonKey + }, + () => { + editor.emit('editor.ready'); + isReset && editor.emit('ide.afterReset'); + } + ); + }); + }; + + render() { + const { initReady, skeletonKey, __hasError } = this.state; + if (__hasError || !this.editor) { + return 'error'; + } + + return ( + + { + const { location, history, match } = props; + location.query = parseSearch(location.search); + this.editor.set('location', location); + this.editor.set('history', history); + this.editor.set('match', match); + console.log('&&&&&&&&&&'); + return ( + + +
+ +
+ + + + +
+
+
+
+ ); + }} + /> +
+ ); + } +} diff --git a/packages/editor/tests/index.js b/packages/editor/tests/index.js new file mode 100644 index 000000000..346e384d2 --- /dev/null +++ b/packages/editor/tests/index.js @@ -0,0 +1 @@ +// test file diff --git a/packages/editor/tsconfig.json b/packages/editor/tsconfig.json new file mode 100644 index 000000000..3f5e62810 --- /dev/null +++ b/packages/editor/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compileOnSave": false, + "buildOnSave": false, + "compilerOptions": { + "baseUrl": ".", + "outDir": "build", + "module": "esnext", + "target": "es6", + "jsx": "react", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "lib": ["es6", "dom"], + "sourceMap": true, + "allowJs": true, + "rootDir": "src", + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": false, + "importHelpers": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "noUnusedLocals": true, + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/*"], + "exclude": ["node_modules", "build", "public"] +}