@studio-kit/utils-browser 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Socket.js ADDED
@@ -0,0 +1,263 @@
1
+ import ReconnectingWebSocket from 'reconnecting-websocket'
2
+
3
+ const WS_OPEN = 1
4
+ const WS_CLOSED = 3
5
+ const SEND_QUEUE_MAX = 50
6
+
7
+ export default class Socket {
8
+ static createClient({
9
+ url,
10
+ debug,
11
+ maxRetries = 10,
12
+ connectionTimeout = 1000,
13
+ eventMap = {},
14
+ parseMessage,
15
+ onOpen,
16
+ onClose,
17
+ onError,
18
+ } = {}) {
19
+ let closed = false
20
+ let ws = new ReconnectingWebSocket(url, null, {
21
+ connectionTimeout,
22
+ maxRetries,
23
+ debug,
24
+ })
25
+ const sendQueue = []
26
+ let openHandler = onOpen
27
+ let closeHandler = onClose
28
+ let errorHandler = onError
29
+
30
+ ws.onopen = () => {
31
+ if (closed) return
32
+ console.log('socket opened', url)
33
+ flushSendQueue()
34
+ openHandler?.()
35
+ }
36
+ ws.onclose = function () {
37
+ if (closed) return
38
+ console.log('socket closed', url)
39
+ closeHandler?.()
40
+ }
41
+ ws.onmessage = function (msg) {
42
+ dispatchMessage(msg)
43
+ }
44
+ ws.onerror = (err) => {
45
+ if (closed) return
46
+ if (debug !== false) console.error(err)
47
+ errorHandler?.(err)
48
+ }
49
+
50
+ const result = {
51
+ get url() {
52
+ return url
53
+ },
54
+ get readyState() {
55
+ if (closed || !ws) return WS_CLOSED
56
+ return ws.readyState
57
+ },
58
+ send({ data }) {
59
+ if (closed || !ws) {
60
+ console.warn('socket 已关闭,无法发送', url)
61
+ return result
62
+ }
63
+ const payload =
64
+ typeof data === 'object' && data !== null
65
+ ? JSON.stringify(data)
66
+ : data
67
+ if (ws.readyState === WS_OPEN) {
68
+ ws.send(payload)
69
+ } else if (sendQueue.length >= SEND_QUEUE_MAX) {
70
+ console.warn('socket 发送队列已满,已丢弃消息', url)
71
+ } else {
72
+ sendQueue.push(payload)
73
+ }
74
+ return result
75
+ },
76
+ addEventListener({ messageKey, messageHandler }) {
77
+ if (closed) {
78
+ console.warn('socket 已关闭,无法监听', url)
79
+ return function unsubscribe() {}
80
+ }
81
+ assertHandler(messageHandler)
82
+ if (!eventMap[messageKey]) {
83
+ eventMap[messageKey] = []
84
+ }
85
+ if (!eventMap[messageKey].includes(messageHandler)) {
86
+ eventMap[messageKey].push(messageHandler)
87
+ }
88
+ return function unsubscribe() {
89
+ result.removeEventListener({ messageKey, messageHandler })
90
+ }
91
+ },
92
+ addEventListeners({ configs }) {
93
+ const list = configs || []
94
+ list.forEach((config) => {
95
+ result.addEventListener(config)
96
+ })
97
+ return function removeAll() {
98
+ list.forEach((config) => {
99
+ result.removeEventListener(config)
100
+ })
101
+ }
102
+ },
103
+ removeEventListener({ messageKey, messageHandler }) {
104
+ const handlers = eventMap[messageKey]
105
+ if (!handlers) return
106
+ const next = handlers.filter((fn) => fn !== messageHandler)
107
+ if (next.length) {
108
+ eventMap[messageKey] = next
109
+ } else {
110
+ delete eventMap[messageKey]
111
+ }
112
+ },
113
+ close() {
114
+ destroyClient()
115
+ },
116
+ destroy() {
117
+ destroyClient()
118
+ },
119
+ }
120
+ return result
121
+
122
+ function destroyClient() {
123
+ if (closed) return
124
+ closed = true
125
+ sendQueue.length = 0
126
+ Object.keys(eventMap).forEach((key) => {
127
+ delete eventMap[key]
128
+ })
129
+ openHandler = null
130
+ closeHandler = null
131
+ errorHandler = null
132
+ if (ws) {
133
+ ws.onopen = null
134
+ ws.onmessage = null
135
+ ws.onerror = null
136
+ ws.onclose = null
137
+ try {
138
+ ws.maxRetries = 0
139
+ ws.close()
140
+ } catch (_) {}
141
+ ws = null
142
+ }
143
+ }
144
+
145
+ function flushSendQueue() {
146
+ if (closed || !ws) return
147
+ while (sendQueue.length && ws.readyState === WS_OPEN) {
148
+ ws.send(sendQueue.shift())
149
+ }
150
+ }
151
+
152
+ function dispatchMessage(msg) {
153
+ if (closed) return
154
+ const { data } = msg
155
+ if (typeof data !== 'string') return
156
+ const raw = data.trim()
157
+ if (!raw.startsWith('{')) return
158
+ let parsed
159
+ try {
160
+ parsed = JSON.parse(raw)
161
+ } catch (err) {
162
+ console.error('socket JSON parse error', err)
163
+ return
164
+ }
165
+ const resolved = resolveMessage(parsed, raw)
166
+ if (!resolved) return
167
+ const { messageKey, body } = resolved
168
+ const handlers = eventMap[messageKey]
169
+ if (handlers?.length) {
170
+ handlers.slice().forEach((fn) => fn(body))
171
+ return
172
+ }
173
+ const fallback = eventMap['*']
174
+ if (fallback?.length) {
175
+ fallback.slice().forEach((fn) => fn(body))
176
+ }
177
+ }
178
+
179
+ function resolveMessage(parsed, raw) {
180
+ if (typeof parseMessage === 'function') {
181
+ return parseMessage({ data: parsed, raw })
182
+ }
183
+ return {
184
+ messageKey: parsed.key ?? parsed.topic,
185
+ body: parsed.body ?? parsed.payload ?? parsed,
186
+ }
187
+ }
188
+ }
189
+
190
+ static create({ configs, isDev } = {}) {
191
+ const clientMap = {}
192
+ normalizeConfigs(configs).forEach((config) => {
193
+ const { key, devUrl, url } = config
194
+ const resolvedUrl = isDev && devUrl ? devUrl : url
195
+ if (!resolvedUrl) {
196
+ console.warn(`websocket 未配置 url,已跳过:${key}`)
197
+ return
198
+ }
199
+ clientMap[key] = Socket.createClient({
200
+ url: resolvedUrl,
201
+ debug: config.debug,
202
+ maxRetries: config.maxRetries,
203
+ connectionTimeout: config.connectionTimeout,
204
+ parseMessage: config.parseMessage,
205
+ onOpen: config.onOpen,
206
+ onClose: config.onClose,
207
+ onError: config.onError,
208
+ })
209
+ })
210
+
211
+ const hub = {
212
+ addEventListener({ socketKey, messageKey, messageHandler }) {
213
+ const client = getClient(socketKey)
214
+ return client.addEventListener({ messageKey, messageHandler })
215
+ },
216
+ removeEventListener({ socketKey, messageKey, messageHandler }) {
217
+ const client = getClient(socketKey)
218
+ client.removeEventListener({ messageKey, messageHandler })
219
+ },
220
+ send({ socketKey, data }) {
221
+ const client = getClient(socketKey)
222
+ return client.send({ data })
223
+ },
224
+ has({ socketKey }) {
225
+ return Boolean(clientMap[socketKey])
226
+ },
227
+ getClient({ socketKey }) {
228
+ return getClient(socketKey)
229
+ },
230
+ close({ socketKey }) {
231
+ const client = getClient(socketKey)
232
+ client.destroy()
233
+ delete clientMap[socketKey]
234
+ },
235
+ closeAll() {
236
+ Object.keys(clientMap).forEach((key) => {
237
+ clientMap[key].destroy()
238
+ delete clientMap[key]
239
+ })
240
+ },
241
+ }
242
+ return hub
243
+
244
+ function getClient(key) {
245
+ const client = clientMap[key]
246
+ if (!client) throw new Error(`对应 websocket client 不存在:${key}`)
247
+ return client
248
+ }
249
+ }
250
+ }
251
+
252
+ function assertHandler(messageHandler) {
253
+ if (typeof messageHandler !== 'function') {
254
+ throw new TypeError('messageHandler 必须为函数')
255
+ }
256
+ }
257
+
258
+ function normalizeConfigs(configs) {
259
+ if (!configs) return []
260
+ if (Array.isArray(configs)) return configs
261
+ if (typeof configs === 'object') return [configs]
262
+ return []
263
+ }
package/src/Style.js CHANGED
@@ -1,12 +1,38 @@
1
1
  import chroma from 'chroma-js';
2
- export default class Style {
3
- static getCssVar({ varKey }) {
2
+ import Data from './Data.js'
3
+ import CommonStyle from '@studio-kit/utils-common/Style.js'
4
+ export default class Style extends CommonStyle {
5
+ static getCssVar({ key, varKey }) {
6
+ if (!varKey) {
7
+ varKey = key
8
+ }
4
9
  const root = document.documentElement;
5
10
  const computedStyle = window.getComputedStyle(root);
6
11
  // 获取CSS变量的值
7
12
  const varValue = computedStyle.getPropertyValue(varKey);
8
13
  return varValue
9
14
  }
15
+ static setCssVar({ key, value }) {
16
+ // 获取:root元素(即document.documentElement)
17
+ const root = document.documentElement;
18
+
19
+ // 设置CSS变量
20
+ if (!/^\-\-/.test(key)) {
21
+ key = `--${key}`
22
+ }
23
+ root.style.setProperty(key, value);
24
+ }
25
+ static color(value) {
26
+ if (value) {
27
+ value = value.replace(/[\s\t\n]/g, '')
28
+ }
29
+ if (/^rgb/.test(value)) {
30
+ const numList = value.matchAll(/\d+/g)
31
+ value = [...numList];
32
+ console.log('value', value)
33
+ }
34
+ return chroma(value)
35
+ }
10
36
  static getThemeImageFilter({ themeColor, imageColor = '#000000' }) {
11
37
  const hue2 = this.createColor(themeColor).hsl()[0]
12
38
  let hue1 = this.createColor(imageColor).hsl()[0]
@@ -18,4 +44,49 @@ export default class Style {
18
44
  cssColorString = cssColorString.replace(/\s/g, '')
19
45
  return chroma(cssColorString)
20
46
  }
21
- }
47
+ static setGlobalStyle({ cssText, id }) {
48
+ if (!id) {
49
+ id = 'style-' + Data.createId()
50
+ }
51
+ // 查找是否已存在相同id的style元素
52
+ let style = document.getElementById(id);
53
+ if (style) {
54
+ // 如果存在,直接替换内容
55
+ style.textContent = cssText;
56
+ } else {
57
+ // 如果不存在,创建新的style元素
58
+ style = document.createElement('style');
59
+ style.id = id;
60
+ style.textContent = cssText;
61
+ document.head.appendChild(style);
62
+ }
63
+ return style;
64
+ }
65
+ static getSize({ value }) {
66
+ if (Data.isNumberOrStringNumber(value)) {
67
+ return value + 'px'
68
+ } else {
69
+ return value
70
+ }
71
+ }
72
+ static measureText({ text, style, className }) {
73
+ const el = document.createElement('span')
74
+ el.textContent = text
75
+ el.style.position = 'absolute'
76
+ el.style.visibility = 'hidden'
77
+ el.style.whiteSpace = 'nowrap'
78
+ if (className) {
79
+ el.className = className
80
+ }
81
+ if (style) {
82
+ Object.assign(el.style, style)
83
+ }
84
+ document.body.appendChild(el)
85
+ const rect = el.getBoundingClientRect()
86
+ document.body.removeChild(el)
87
+ return {
88
+ width: rect.width,
89
+ height: rect.height
90
+ }
91
+ }
92
+ }
package/src/UserInput.js CHANGED
@@ -1,7 +1,6 @@
1
1
  const defaultWindow = window
2
2
  const useEventListener = (target, type, listener, options) => {
3
3
  target = target || defaultWindow
4
- console.log('target', target)
5
4
  target.addEventListener(type, listener, options)
6
5
  return () => target.removeEventListener(type, listener, options)
7
6
  }
package/src/cta.js CHANGED
@@ -138,7 +138,7 @@ function cta(trigger, target, options, callback) {
138
138
  dummy.style.setProperty('position', (options.relativeToWindow ? 'fixed' : 'absolute'), 'important');
139
139
  dummy.style.setProperty('-webkit-transform-origin', 'top left', 'important');
140
140
  dummy.style.setProperty('transform-origin', 'top left', 'important');
141
- dummy.style.setProperty('transition', options.duration + 's ease');
141
+ dummy.style.setProperty('transition', 'transform ' + options.duration + 's ease, background .3s ease-in');
142
142
 
143
143
  // Set dummy element's dimensions to final state.
144
144
  dummy.style.setProperty('width', targetBounds.width + 'px', 'important');
@@ -146,7 +146,8 @@ function cta(trigger, target, options, callback) {
146
146
  dummy.style.setProperty('left', (targetBounds.left + (options.relativeToWindow ? 0 : window.pageXOffset)) + 'px', 'important');
147
147
  dummy.style.setProperty('top', (targetBounds.top + (options.relativeToWindow ? 0 : window.pageYOffset)) + 'px', 'important');
148
148
  dummy.style.setProperty('background', triggerBackground, 'important');
149
- dummy.style.setProperty('z-index', 1, 'important');
149
+ dummy.style.setProperty('z-index', 1e5, 'important');
150
+ dummy.style.setProperty('border-radius', getComputedStyle(trigger).borderRadius);
150
151
 
151
152
  // Apply a reverse transform to bring back dummy element to the dimensions of the trigger/starting element.
152
153
  // Credits: This technique is inspired by Paul Lewis: http://aerotwist.com/blog/flip-your-animations/ He is amazing!
@@ -1,13 +1,9 @@
1
- export function watchSizeChange(element, onChange) {
2
- if (window.ResizeObserver)
3
- return new ResizeObserver(onChange).observe(element)
1
+ import DOM from '../DOM.js';
2
+
3
+ export function watchSizeChange(element, onChange, options) {
4
+ return DOM.watchSizeChange(element, onChange, options);
4
5
  }
5
6
 
6
7
  export function unwatchSizeChange(watcher, element) {
7
- if (window.ResizeObserver) {
8
- if (watcher instanceof window.ResizeObserver) {
9
- if (element) watcher.unobserve(element);
10
- else watcher.disconnect();
11
- }
12
- }
13
- }
8
+ return DOM.unwatchSizeChange(watcher, element);
9
+ }
@@ -1,9 +1,12 @@
1
1
  const loadingMap = {};
2
2
  const loadedMap = {};
3
3
  export default function (src, key, type) {
4
- if (key && window[key]) return Promise.resolve(window['key']);
5
- if (loadedMap[src]) return Promise.resolve();
4
+ if (key && window[key]) return Promise.resolve(window[key]);
6
5
  if (loadingMap[src]) return loadingMap[src];
6
+ if (Object.prototype.hasOwnProperty.call(loadedMap, src)) {
7
+ const cached = loadedMap[src];
8
+ return Promise.resolve(cached === true ? undefined : cached);
9
+ }
7
10
  if (/\.js$/.test(src) || type === 'js')
8
11
  return loadingMap[src] = new Promise((resolve, reject) => {
9
12
  const script = document.createElement('script');
@@ -38,4 +41,23 @@ export default function (src, key, type) {
38
41
  document.head.appendChild(link);
39
42
  });
40
43
  }
44
+ if (/\.json$/.test(src) || type === 'json') {
45
+ return loadingMap[src] = fetch(src)
46
+ .then((res) => {
47
+ if (!res.ok) {
48
+ throw new Error(`加载json文件错误(src: ${src})(status: ${res.status})`);
49
+ }
50
+ return res.json();
51
+ })
52
+ .then((data) => {
53
+ delete loadingMap[src];
54
+ loadedMap[src] = data;
55
+ return data;
56
+ })
57
+ .catch((err) => {
58
+ delete loadingMap[src];
59
+ console.error(err);
60
+ throw err;
61
+ });
62
+ }
41
63
  }
@@ -0,0 +1,67 @@
1
+ /** 标准弹窗相对触发元素或视口的位置 */
2
+ export type AppOpenStdWinPosition = 'center' | 'near-source';
3
+
4
+ /** 标准弹窗类型 */
5
+ export type AppOpenStdWinType = 'confirm';
6
+
7
+ /**
8
+ * Vue 内容组件:SFC 默认导出、全局组件名或懒加载函数。
9
+ * 未使用 `component` 时可改用 `pageKey`(运行时映射为 component)。
10
+ */
11
+ export type AppOpenStdWinComponent =
12
+ | string
13
+ | Record<string, unknown>
14
+ | (() => Promise<unknown>);
15
+
16
+ /** 确认弹窗内组件实例($refs 上的窗口组件) */
17
+ export interface AppOpenStdWinConfirmComponent<TResult = unknown> {
18
+ getResult?: () => TResult;
19
+ data?: unknown;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ /** `APP.openStdWin` 参数(经 $bus 传给 std-wins,`props` 会映射为 `componentProps`) */
24
+ export interface AppOpenStdWinParams<
25
+ TProps extends Record<string, unknown> = Record<string, unknown>,
26
+ TResult = unknown,
27
+ > {
28
+ /** 鼠标事件,用于 `near-source` 时取 `currentTarget` 作为锚点 */
29
+ mouseEvent?: MouseEvent;
30
+ /** 锚点元素;未传且提供 `mouseEvent` 时使用 `mouseEvent.currentTarget` */
31
+ sourceEl?: Element;
32
+ /** 窗口位置 */
33
+ position?: AppOpenStdWinPosition;
34
+ /** 窗口标题 */
35
+ title: string;
36
+ /** 宽度(px 数字或如 `'60%'` 的字符串) */
37
+ width?: number | string;
38
+ /** 高度 */
39
+ height?: number | string;
40
+ /** 内容组件 */
41
+ component?: AppOpenStdWinComponent;
42
+ /** 页面/全局组件 key,与 `component` 二选一 */
43
+ pageKey?: string;
44
+ /** 传给内容组件的 props */
45
+ props?: TProps;
46
+ /** 窗口类型 */
47
+ type?: AppOpenStdWinType;
48
+ /** 确定按钮文案 */
49
+ confirmText?: string;
50
+ /** 是否显示遮罩 */
51
+ hasMask?: boolean;
52
+ /** 窗口 id;已存在则 focus 而非新建 */
53
+ id?: string;
54
+ /** 确认回调;`close` 由调用方在逻辑结束后执行 */
55
+ afterConfirm?: (
56
+ compo: AppOpenStdWinConfirmComponent<TResult>,
57
+ close: () => void,
58
+ ) => void | Promise<void>;
59
+ /** WinBox 挂载完成后的钩子 */
60
+ afterWinMounted?: (ctx: { winbox: unknown }) => void;
61
+ }
62
+
63
+ /** @alias AppOpenStdWinParams */
64
+ export type StdAppOpenStdWinParams<
65
+ TProps extends Record<string, unknown> = Record<string, unknown>,
66
+ TResult = unknown,
67
+ > = AppOpenStdWinParams<TProps, TResult>;
@@ -7,10 +7,6 @@ import {
7
7
  unwatchSizeChange,
8
8
  } from './methods/dom'
9
9
 
10
- import {
11
- emitParentIframe
12
- } from './methods/message'
13
-
14
10
  import {
15
11
  debounce
16
12
  } from './methods/perfomance'
@@ -28,6 +24,7 @@ export default function (Vue, config = {}) {
28
24
  debounce,
29
25
  }
30
26
  const { getUserInfo, clientType, isDev } = config
27
+
31
28
  const $api = {
32
29
  runtimeBaseURL: '/studio-server',
33
30
  getUserInfo,
@@ -77,7 +74,7 @@ export default function (Vue, config = {}) {
77
74
  })
78
75
  }
79
76
  })
80
- Vue.prototype.$api = $api
77
+ Vue.prototype.$api = $api
81
78
  Vue.prototype.$app = {
82
79
  baseUrl: '/studio-server',
83
80
  $compoPrefix: 'studio-',
@@ -1,5 +0,0 @@
1
- export function emitParentIframe({ data, origin = '*' }) {
2
- console.log('emitParentIframe', data)
3
- if (window.parent) window.parent.postMessage(data, origin);
4
- else console.error('对应父级iframe不存在')
5
- }