layout.js 11 KB

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