layout.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import { toPx, isNumber, getImageInfo } from './utils'
  2. let uuid = 0;
  3. export class Layout {
  4. constructor(context, root, isH5PathToBase64) {
  5. this.ctx = context
  6. this.root = root
  7. this.isH5PathToBase64 = isH5PathToBase64
  8. }
  9. init(context, root, isH5PathToBase64) {
  10. this.ctx = context
  11. this.root = root
  12. this.isH5PathToBase64 = isH5PathToBase64
  13. }
  14. async getNodeTree(element, parent = {}, index = 0, siblings = [], source) {
  15. let computedStyle = Object.assign({}, this.getComputedStyle(element, parent, index));
  16. let attributes = await this.getAttributes(element)
  17. let node = {
  18. id: uuid++,
  19. parent,
  20. computedStyle,
  21. rules: element.rules,
  22. attributes: Object.assign({}, attributes),
  23. name: element?.type || 'view',
  24. use:element?.use || 'view',
  25. }
  26. if(JSON.stringify(parent) === '{}' && !element.type) {
  27. const {left = 0, top = 0, width = 0, height = 0 } = computedStyle
  28. node.layoutBox = {left, top, width, height }
  29. } else {
  30. node.layoutBox = Object.assign({left: 0, top: 0}, this.getLayoutBox(node, parent, index, siblings, source))
  31. }
  32. if (element?.views) {
  33. let childrens = []
  34. node.children = []
  35. for (let i = 0; i < element.views.length; i++) {
  36. console.log(childrens,'childrens')
  37. let v = element.views[i]
  38. childrens.push(await this.getNodeTree(v, node, i, childrens, element))
  39. }
  40. node.children = childrens
  41. }
  42. return node
  43. }
  44. getComputedStyle(element, parent = {}, index = 0) {
  45. const style = {}
  46. const name = element.name || element.type
  47. const node = JSON.stringify(parent) == '{}' && !name ? element : element.css;
  48. if(!node) return style
  49. const inheritProps = ['color', 'fontSize', 'lineHeight', 'verticalAlign', 'fontWeight', 'textAlign']
  50. if(parent.computedStyle) {
  51. inheritProps.forEach(prop => {
  52. if(node[prop] || parent.computedStyle[prop]) {
  53. node[prop] = node[prop] || parent.computedStyle[prop]
  54. }
  55. })
  56. }
  57. for (let value of Object.keys(node)) {
  58. const item = node[value]
  59. if(value == 'views') {continue}
  60. if (/^(box)?shadow$/i.test(value)) {
  61. let shadows = item.split(' ').map(v => /^\d/.test(v) ? toPx(v) : v)
  62. style.boxShadow = shadows
  63. continue
  64. }
  65. if (/^border(?!radius)/i.test(value)) {
  66. const prefix = value.match(/^border([BTRLa-z]+)?/)[0]
  67. const type = value.match(/[W|S|C][a-z]+/)
  68. let v = item.split(' ').map(v => /^\d/.test(v) ? toPx(v) : v)
  69. if(v.length > 1) {
  70. style[prefix] = {
  71. [prefix + 'Width'] : v[0] || 1,
  72. [prefix + 'Style'] : v[1] || 'solid',
  73. [prefix + 'Color'] : v[2] || 'black'
  74. }
  75. } else {
  76. style[prefix] = {
  77. [prefix + 'Width'] : 1,
  78. [prefix + 'Style'] : 'solid',
  79. [prefix + 'Color'] : 'black'
  80. }
  81. style[prefix][prefix + type[0]] = v[0]
  82. }
  83. continue
  84. }
  85. if (/^background(Color)?$/i.test(value)) {
  86. style['backgroundColor'] = item
  87. continue
  88. }
  89. if(/padding|margin|radius/i.test(value)) {
  90. let isRadius = value.includes('adius')
  91. let prefix = isRadius ? 'borderRadius' : value.match(/[a-z]+/)[0]
  92. let pre = [0,0,0,0].map((item, i) => isRadius ? ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius'][i] : [prefix + 'Top', prefix + 'Right', prefix + 'Bottom', prefix + 'Left'][i] )
  93. if(value === 'padding' || value === 'margin' || value === 'radius' || value === 'borderRadius') {
  94. let v = item?.split(' ').map((item) => /^\d/.test(item) && toPx(item, node['width']), []) ||[0];
  95. let type = isRadius ? 'borderRadius' : value;
  96. if(v.length == 1) {
  97. style[type] = v[0]
  98. } else {
  99. let [t, r, b, l] = v
  100. style[type] = {
  101. [pre[0]]: t,
  102. [pre[1]]: isNumber(r) ? r : t,
  103. [pre[2]]: isNumber(b) ? b : t,
  104. [pre[3]]: isNumber(l) ? l : r
  105. }
  106. }
  107. } else {
  108. if(typeof style[prefix] === 'object') {
  109. style[prefix][value] = toPx(item, node['width'])
  110. } else {
  111. style[prefix] = {
  112. [pre[0]]: style[prefix] || 0,
  113. [pre[1]]: style[prefix] || 0,
  114. [pre[2]]: style[prefix] || 0,
  115. [pre[3]]: style[prefix] || 0
  116. }
  117. style[prefix][value] = toPx(item, node['width'])
  118. }
  119. }
  120. continue
  121. }
  122. if(/^(width|height)$/i.test(value)) {
  123. if(/%$/.test(item)) {
  124. style[value] = toPx(item, parent.layoutBox[value])
  125. } else {
  126. style[value] = /px|rpx$/.test(item) ? toPx(item) : item
  127. }
  128. continue
  129. }
  130. if(/^transform$/i.test(value)) {
  131. if(!item){ item = 'translate(0%,0)' }
  132. style[value]= {}
  133. item.replace(/([a-zA-Z]+)\(([0-9,-\.%rpxdeg\s]+)\)/g, (g1, g2, g3) => {
  134. const v = g3.split(',').map(k => k.replace(/(^\s*)|(\s*$)/g,''))
  135. const transform = (v, r) => {
  136. return v.includes('deg') ? v * 1 : r && !/%$/.test(r) ? toPx(v, r) : v
  137. }
  138. if(g2.includes('matrix')) {
  139. style[value][g2] = v.map(v => v * 1)
  140. } else if(g2.includes('rotate')) {
  141. style[value][g2] = g3.match(/\d+/)[0] * 1
  142. }else if(/[X, Y]/.test(g2)) {
  143. style[value][g2] = /[X]/.test(g2) ? transform(v[0], node['width']) : transform(v[0], node['height'])
  144. } else {
  145. style[value][g2+'X'] = transform(v[0], node['width'] )
  146. style[value][g2+'Y'] = transform(v[1] || v[0], node['height'])
  147. }
  148. })
  149. continue
  150. }
  151. if(/%/.test(item)) {
  152. const {width: pw, height: ph, left: pl, top: pt} = parent.layoutBox;
  153. const {width: rw, height: rh} = this.root;
  154. const isR = style.position == 'relative'
  155. if(value == 'width') {
  156. style[value] = toPx(item, pw || rw)
  157. }else if(value == 'height') {
  158. style[value] = toPx(item, ph || rh)
  159. }else if(value == 'left') {
  160. style[value] = item // isR ? toPx(item, pw) + pl : toPx(item, rw)
  161. }else if(value == 'top') {
  162. style[value] = item // isR ? toPx(item, ph) + pt : toPx(item, rh)
  163. } else {
  164. style[value] = toPx(item, node['width'])
  165. }
  166. } else {
  167. style[value] = /px|rpx$/.test(item) ? toPx(item) : /em$/.test(item) && name == 'text'? toPx(item, node['fontSize']) : item
  168. }
  169. }
  170. if(/image/.test(element.name||element.type ) && !style.mode) {
  171. style.mode = element.mode || 'scaleToFill'
  172. if((!node.width || node.width == 'auto') && (!node.height || node.width == 'auto') ) {
  173. style.mode = ''
  174. }
  175. }
  176. return style
  177. }
  178. getLayoutBox(element, parent = {}, index = 0, siblings = [], source = {}) {
  179. let box = {}
  180. let {name, computedStyle: cstyle, layoutBox, attributes} = element || {}
  181. if(!name) return box
  182. const {ctx} = this
  183. const pbox = parent.layoutBox || this.root
  184. const pstyle = parent.computedStyle
  185. let {
  186. verticalAlign: v,
  187. left: x,
  188. top: y,
  189. width: w,
  190. height: h,
  191. fontSize = 14,
  192. lineHeight = '1.4em',
  193. maxLines,
  194. fontWeight,
  195. fontFamily,
  196. textStyle,
  197. position,
  198. display
  199. } = cstyle;
  200. const p = cstyle.padding
  201. const m = cstyle.margin
  202. const { paddingTop: pt = 0, paddingRight: pr = 0, paddingBottom: pb = 0, paddingLeft: pl = 0, } = cstyle.padding || {p,p,p,p}
  203. const { marginTop: mt = 0, marginRight: mr = 0, marginBottom: mb = 0, marginLeft: ml = 0, } = cstyle.margin || {m,m,m,m}
  204. const {layoutBox: lbox, computedStyle: ls, name: lname} = siblings[index - 1] || {}
  205. const {layoutBox: rbox, computedStyle: rs, name: rname} = siblings[index + 1] || {}
  206. const lmb = ls?.margin?.marginBottom || 0
  207. const lmr = ls?.margin?.marginRight || 0
  208. if(/%$/.test(x)) {
  209. x = toPx(x, pbox.width)
  210. }
  211. if(/%$/.test(y)) {
  212. y = toPx(y, pbox.height)
  213. }
  214. if(position == 'relative') {
  215. x += pbox.left
  216. y += pbox.top
  217. }
  218. if(name === 'text') {
  219. const text = attributes.text ||''
  220. lineHeight = toPx(lineHeight, fontSize)
  221. ctx.save()
  222. ctx.setFonts({fontFamily, fontSize, fontWeight, textStyle})
  223. const isLeft = index == 0
  224. const islineBlock = display === 'inlineBlock'
  225. const isblock = display === 'block' || ls?.display === 'block'
  226. const isOnly = isLeft && !rbox || !parent?.id
  227. const lboxR = isLeft || isblock ? 0 : lbox.offsetRight || 0
  228. let texts = text.split('\n')
  229. let lineIndex = 1
  230. let line = ''
  231. const textIndent = cstyle.textIndent || 0
  232. if(!isOnly && !islineBlock) {
  233. texts.map((t, i) => {
  234. lineIndex += i
  235. const chars = t.split('')
  236. for (let j = 0; j < chars.length; j++) {
  237. let ch = chars[j]
  238. let textline = line + ch
  239. let textWidth = ctx.measureText(textline, fontSize).width
  240. if(lineIndex == 1) {
  241. textWidth = textWidth + (isblock ? 0 : lboxR) + textIndent
  242. }
  243. if(textWidth > pbox.width) {
  244. lineIndex++
  245. line = ch
  246. } else {
  247. line = textline
  248. }
  249. }
  250. })
  251. } else {
  252. line = text
  253. lineIndex = Math.max(texts.length, Math.ceil(ctx.measureText(text, fontSize).width / ((w || pbox.width) - ctx.measureText('!', fontSize).width / 2)))
  254. }
  255. if(!islineBlock) {
  256. box.offsetLeft = (isNumber(x) || isblock || isOnly ? textIndent : lboxR) + pl + ml;
  257. }
  258. // 剩下的字宽度
  259. const remain = ctx.measureText(line, fontSize).width
  260. let width = lineIndex > 1 ? pbox.width : remain + (box?.offsetLeft || 0);
  261. if(!islineBlock) {
  262. box.offsetRight = (x || 0) + box.offsetLeft + (w ? w : (isblock ? pbox.width : remain)) + pr + mr;
  263. }
  264. const lboxOffset = lbox ? lbox.left + lbox.width : 0;
  265. const _getLeft = () => {
  266. if(islineBlock) {
  267. return (lboxOffset + width > pbox.width || isLeft ? pbox.left : lboxOffset + lmr ) + ml
  268. }
  269. return (x || pbox.left)
  270. }
  271. const _getWidth = () => {
  272. if(islineBlock) {
  273. return width + pl + pr
  274. }
  275. return w || (!isOnly || isblock ? pbox.width : (width > pbox.width - box.left || lineIndex > 1 ? pbox.width - box.left : width))
  276. }
  277. const _getHeight = () => {
  278. if(h) {
  279. return h
  280. } else if(lineIndex > 1 ) {
  281. return (maxLines || lineIndex) * lineHeight + pt + pb
  282. } else {
  283. return lineHeight + pt + pb
  284. }
  285. }
  286. const _getTop = () => {
  287. let _y = y
  288. if(_y) {
  289. // return _y + pt + mt
  290. } else if(isLeft) {
  291. _y = pbox.top
  292. } else if((lineIndex == 1 && width < pbox.width && lname === 'text' && !isblock && !islineBlock) || lbox.width < pbox.width && !(islineBlock && (lboxOffset + width > pbox.width))) {
  293. _y = lbox.top
  294. } else {
  295. _y = lbox.top + lbox.height - (ls?.lineHeight || 0)
  296. }
  297. if (v === 'bottom') {
  298. _y = pbox.top + (pbox.height - box.height || 0)
  299. }
  300. if (v === 'middle') {
  301. _y = pbox.top + (pbox.height ? (pbox.height - box.height || 0) / 2 : 0)
  302. }
  303. return _y + mt + (isblock && ls?.lineHeight || 0 ) + (lboxOffset + width > pbox.width ? lmb : 0)
  304. }
  305. box.left = _getLeft()
  306. box.width = _getWidth()
  307. box.height = _getHeight()
  308. box.top = _getTop()
  309. if(pstyle && !pstyle.height) {
  310. pbox.height = box.top - pbox.top + box.height
  311. }
  312. ctx.restore()
  313. } else if(['view', 'qrcode'].includes(name)) {
  314. box.left = ( x || pbox.left) + ml - mr
  315. box.width = (w || pbox?.width) - pl - pr
  316. box.height = (h || 0 )
  317. if(isNumber(y)) {
  318. box.top = y + mt
  319. } else {
  320. box.top = (lbox && (lbox.top + lbox.height) || pbox.top) + mt + lmb
  321. }
  322. } else if(name === 'image') {
  323. const {
  324. width: rWidth,
  325. height: rHeight
  326. } = attributes
  327. const limageOffset = lbox && (lbox.left + lbox.width)
  328. if(isNumber(x)) {
  329. box.left = x + ml - mr
  330. } else {
  331. box.left = (lbox && (limageOffset < pbox.width ? limageOffset : pbox.left) || pbox.left) + ml - mr
  332. }
  333. if(isNumber(w)) {
  334. box.width = w // - pl - pr
  335. } else {
  336. box.width = Math.round(isNumber(h) ? rWidth * h / rHeight : pbox?.width) // - pl - pr
  337. }
  338. if(isNumber(h)) {
  339. box.height = h
  340. } else {
  341. const cH = Math.round(box.width * rHeight / rWidth )
  342. box.height = Math.min(cH, pbox?.height)
  343. }
  344. if(isNumber(y)) {
  345. box.top = y + mt
  346. } else {
  347. box.top = (lbox && (limageOffset < pbox.width ? limageOffset : (lbox.top + lbox.height)) || pbox.top) + mt + lmb
  348. }
  349. }
  350. return box
  351. }
  352. async getAttributes(element) {
  353. let arr = { }
  354. if(element?.url || element?.src) {
  355. arr.src = element.url || element?.src;
  356. const {width = 0, height = 0, path: src, url} = await getImageInfo(arr.src, this.isH5PathToBase64) || {}
  357. arr = Object.assign({}, arr, {width, height, src, url})
  358. }
  359. if(element?.text) {
  360. arr.text = element.text
  361. }
  362. return arr
  363. }
  364. async calcNode(element) {
  365. const node = element || this.element
  366. return await this.getNodeTree(node)
  367. }
  368. }