layout.js 12 KB

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