Trapped-Frontend.st 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. Smalltalk createPackage: 'Trapped-Frontend'!
  2. (Smalltalk packageAt: 'Trapped-Frontend') imports: {'amber/jquery/Wrappers-JQuery'. 'amber/web/Web'}!
  3. Object subclass: #TrappedDataCarrier
  4. instanceVariableNames: 'target model chain'
  5. package: 'Trapped-Frontend'!
  6. !TrappedDataCarrier methodsFor: 'accessing'!
  7. chain: aProcessingChain
  8. chain := aProcessingChain
  9. !
  10. target
  11. ^target
  12. !
  13. target: anObject
  14. target := anObject
  15. !
  16. value
  17. ^model
  18. !
  19. value: anObject
  20. model := anObject
  21. !
  22. value: anObject whenDifferentFrom: anotherObject
  23. anObject = anotherObject ifFalse: [ self value: anObject ]
  24. ! !
  25. !TrappedDataCarrier methodsFor: 'converting'!
  26. falseAsNilValue
  27. | value |
  28. value := self value.
  29. value = false ifTrue: [ ^nil ] ifFalse: [ ^value ]
  30. ! !
  31. !TrappedDataCarrier methodsFor: 'initialization'!
  32. initialize
  33. super initialize.
  34. model := true
  35. ! !
  36. !TrappedDataCarrier class methodsFor: 'not yet classified'!
  37. on: aProcessingChain target: anObject
  38. ^self new
  39. chain: aProcessingChain;
  40. target: anObject;
  41. yourself
  42. ! !
  43. TrappedDataCarrier subclass: #TrappedDataCarrierToModel
  44. instanceVariableNames: 'index'
  45. package: 'Trapped-Frontend'!
  46. !TrappedDataCarrierToModel methodsFor: 'not yet classified'!
  47. proceed
  48. index := index ifNil: [ chain lastProcessorNo ] ifNotNil: [ index - 1 ].
  49. (chain processorNo: index) toModel: self
  50. ! !
  51. TrappedDataCarrier subclass: #TrappedDataCarrierToView
  52. instanceVariableNames: 'index'
  53. package: 'Trapped-Frontend'!
  54. !TrappedDataCarrierToView methodsFor: 'not yet classified'!
  55. proceed
  56. index := index ifNil: [ chain firstProcessorNo ] ifNotNil: [ index + 1 ].
  57. (chain processorNo: index) toView: self
  58. ! !
  59. Object subclass: #TrappedProcessingChain
  60. instanceVariableNames: 'processors'
  61. package: 'Trapped-Frontend'!
  62. !TrappedProcessingChain methodsFor: 'accessing'!
  63. firstProcessorNo
  64. ^1
  65. !
  66. lastProcessorNo
  67. ^processors size
  68. !
  69. processorNo: aNumber
  70. ^processors at: aNumber
  71. !
  72. processors: anArray
  73. processors := anArray
  74. ! !
  75. !TrappedProcessingChain methodsFor: 'action'!
  76. forSnapshot: aSnapshot andBrush: aTagBrush
  77. | toViewCarrier toModelCarrier |
  78. toViewCarrier := TrappedDataCarrierToView on: self target: aTagBrush.
  79. toModelCarrier := TrappedDataCarrierToModel on: self target: aSnapshot.
  80. processors do: [ :each | each installToView: toViewCarrier toModel: toModelCarrier ].
  81. toViewCarrier value = true ifTrue: [ toViewCarrier copy proceed ]
  82. ! !
  83. !TrappedProcessingChain class methodsFor: 'instance creation'!
  84. new: anArray
  85. (anArray anySatisfy: [ :each | each isExpectingModelData ])
  86. ifFalse: [ anArray add: self dataTerminator ]
  87. ifTrue: [ anArray addFirst: self blackboardReaderWriter ].
  88. ^self new
  89. processors: anArray;
  90. yourself
  91. !
  92. newFromProcessorSpecs: anArray
  93. ^self new: ((anArray ifEmpty: [ #(contents) ]) collect: [ :each | each asTrapProcSendTo: TrappedProcessor ])
  94. ! !
  95. !TrappedProcessingChain class methodsFor: 'private'!
  96. blackboardReaderWriter
  97. ^TrappedProcessorBlackboard new
  98. !
  99. dataTerminator
  100. ^TrappedProcessorTerminator new
  101. ! !
  102. Object subclass: #TrappedProcessor
  103. instanceVariableNames: ''
  104. package: 'Trapped-Frontend'!
  105. !TrappedProcessor commentStamp!
  106. I am a processing step in TrappedProcessingChain.
  107. I am stateless flyweight (aka servant)
  108. and will get all necessary data as arguments in API calls.
  109. My public API is:
  110. - installToView:toModel:
  111. This gets two TrappedDataCarriers set up without actual data
  112. and at the beginning of their chains. It should do one-time
  113. installation task needed (install event handlers etc.).
  114. To start a chain, do: dataCarrier copy value: data; proceed.
  115. - toView:
  116. This performs transformation of TrappedDataCarrier on its way from model to view.
  117. Should call aDataCarrier proceed to proceed to subsequent step.
  118. - toModel:
  119. This performs transformation of TrappedDataCarrier on its way from view to model.
  120. Should call aDataCarrier proceed to proceed to subsequent step.!
  121. !TrappedProcessor methodsFor: 'data transformation'!
  122. toModel: aDataCarrier
  123. "by default, proceed"
  124. aDataCarrier proceed
  125. !
  126. toView: aDataCarrier
  127. "by default, proceed"
  128. aDataCarrier proceed
  129. ! !
  130. !TrappedProcessor methodsFor: 'installation'!
  131. installToView: aDataCarrier toModel: anotherDataCarrier
  132. "by default, do nothing"
  133. ! !
  134. !TrappedProcessor methodsFor: 'testing'!
  135. isExpectingModelData
  136. ^false
  137. ! !
  138. !TrappedProcessor class methodsFor: 'factory'!
  139. contents
  140. ^TrappedProcessorContents new
  141. ! !
  142. TrappedProcessor subclass: #TrappedDataExpectingProcessor
  143. instanceVariableNames: ''
  144. package: 'Trapped-Frontend'!
  145. !TrappedDataExpectingProcessor commentStamp!
  146. I answer true to isExpectingModelData and serve as a base class
  147. for processor that present / change model data.
  148. When at least one of my instances is present in the chain,
  149. automatic databinding processor is added at the beginning
  150. (the data-binding scenario); otherwise, the chain
  151. is run immediately with true as data (run-once scenario).!
  152. !TrappedDataExpectingProcessor methodsFor: 'testing'!
  153. isExpectingModelData
  154. ^true
  155. ! !
  156. TrappedDataExpectingProcessor subclass: #TrappedProcessorContents
  157. instanceVariableNames: ''
  158. package: 'Trapped-Frontend'!
  159. !TrappedProcessorContents commentStamp!
  160. I put data into target via contents: in toView:!
  161. !TrappedProcessorContents methodsFor: 'data transformation'!
  162. toView: aDataCarrier
  163. aDataCarrier toTargetContents
  164. ! !
  165. TrappedProcessor subclass: #TrappedProcessorBlackboard
  166. instanceVariableNames: ''
  167. package: 'Trapped-Frontend'!
  168. !TrappedProcessorBlackboard commentStamp!
  169. I am used internally to fetch data from blackboard
  170. or write it back.
  171. I am added to the beginning of the chain
  172. when the chain contains at least one element
  173. that isExpectingModelData (see TrappedDataExpectingProcessor).!
  174. !TrappedProcessorBlackboard methodsFor: 'data transformation'!
  175. toModel: aDataCarrier
  176. aDataCarrier modifyTarget
  177. ! !
  178. !TrappedProcessorBlackboard methodsFor: 'installation'!
  179. installToView: aDataCarrier toModel: anotherDataCarrier
  180. | snap |
  181. snap := anotherDataCarrier target.
  182. snap watch: [ :data |
  183. (aDataCarrier target asJQuery closest: 'html') toArray isEmpty ifTrue: [ AxonOff signal ].
  184. snap do: [ aDataCarrier copy value: data; proceed ] ].
  185. aDataCarrier value: false
  186. ! !
  187. TrappedProcessor subclass: #TrappedProcessorTerminator
  188. instanceVariableNames: ''
  189. package: 'Trapped-Frontend'!
  190. !TrappedProcessorTerminator commentStamp!
  191. I do not proceed in toView:.
  192. I am added automatically to end of chain when it does not contain
  193. any element that isExpectingModelData (see TrappedDataExpectingProcessor).!
  194. !TrappedProcessorTerminator methodsFor: 'data transformation'!
  195. toView: aDataCarrier
  196. "stop"
  197. ! !
  198. Object subclass: #TrappedSingleton
  199. instanceVariableNames: ''
  200. package: 'Trapped-Frontend'!
  201. !TrappedSingleton methodsFor: 'action'!
  202. start: args
  203. ^ self subclassResponsibility
  204. ! !
  205. TrappedSingleton class instanceVariableNames: 'current'!
  206. !TrappedSingleton class methodsFor: 'accessing'!
  207. current
  208. ^ current ifNil: [ current := self new ]
  209. ! !
  210. !TrappedSingleton class methodsFor: 'action'!
  211. start: args
  212. self current start: args
  213. ! !
  214. TrappedSingleton subclass: #Trapped
  215. instanceVariableNames: 'registry'
  216. package: 'Trapped-Frontend'!
  217. !Trapped methodsFor: 'accessing'!
  218. byName: aString
  219. ^ registry at: aString
  220. !
  221. register: aListKeyedEntity
  222. self register: aListKeyedEntity name: aListKeyedEntity class name
  223. !
  224. register: aListKeyedEntity name: aString
  225. registry at: aString put: aListKeyedEntity
  226. ! !
  227. !Trapped methodsFor: 'action'!
  228. start: args
  229. args do: [ :each | self register: each ].
  230. self injectToElement: document
  231. ! !
  232. !Trapped methodsFor: 'initialization'!
  233. initialize
  234. super initialize.
  235. registry := #{}.
  236. ! !
  237. !Trapped methodsFor: 'private'!
  238. cloneAndInject: anObject
  239. ^anObject asJQuery clone
  240. each: [ :i :each | self injectToElement: each ];
  241. get: 0
  242. !
  243. descend: anArray snapshotDo: aBlock
  244. | tpsc |
  245. tpsc := TrappedPathStack current.
  246. tpsc append: anArray do: [
  247. | prefix modelPath model |
  248. prefix := tpsc elements first.
  249. modelPath := tpsc elements allButFirst.
  250. model := self byName: prefix.
  251. aBlock value: (TrappedSnapshot new prefix: prefix; path: modelPath model: model)
  252. ]
  253. !
  254. injectToChildren: anElement
  255. | child |
  256. child := anElement firstChild.
  257. [ child isNil ] whileFalse: [ self injectToElement: child. child := child nextSibling ]
  258. !
  259. injectToElement: anElement
  260. | jq |
  261. jq := anElement asJQuery.
  262. (jq attr: 'data-trap') ifNotNil: [ :attr |
  263. jq removeAttr: 'data-trap'.
  264. (Trapped parse: attr) do: [ :rule |
  265. (HTMLCanvas onJQuery: jq) root trap: rule first processors: (rule at: 2 ifAbsent: [#()]) ] ].
  266. self injectToChildren: anElement
  267. ! !
  268. !Trapped class methodsFor: 'parsing'!
  269. parse: aString
  270. ^ (aString tokenize: '.') collect: [ :rule |
  271. (rule tokenize: ':') collect: [ :message |
  272. Lyst parse: message ] ]
  273. ! !
  274. !Trapped class methodsFor: 'private'!
  275. loop: aSequenceableCollection before: aNode do: aBlock
  276. aSequenceableCollection withIndexDo: [ :item :i |
  277. | env |
  278. "env := document createDocumentFragment."
  279. env := document createElement: 'ins'.
  280. {i} trapDescend: [ (HTMLCanvas onJQuery: env asJQuery) root with: aBlock ].
  281. "aNode parentNode insertBefore: env reference: aNode"
  282. (Array streamContents: [ :str |
  283. | child |
  284. child := env firstChild.
  285. [ child isNil ] whileFalse: [
  286. str nextPut: child.
  287. child := child nextSibling ]])
  288. do: [ :each | aNode parentNode insertBefore: each reference: aNode ]
  289. ]
  290. !
  291. loop: aSequenceableCollection between: aTagBrush and: anotherTagBrush do: aBlock
  292. (aTagBrush asJQuery nextUntil: anotherTagBrush element) remove.
  293. aSequenceableCollection ifNotNil: [
  294. self loop: aSequenceableCollection before: anotherTagBrush element do: aBlock
  295. ]
  296. ! !
  297. TrappedSingleton subclass: #TrappedPathStack
  298. instanceVariableNames: 'elements'
  299. package: 'Trapped-Frontend'!
  300. !TrappedPathStack methodsFor: 'accessing'!
  301. elements
  302. ^elements
  303. ! !
  304. !TrappedPathStack methodsFor: 'descending'!
  305. append: anArray do: aBlock
  306. self with: elements, anArray do: aBlock
  307. !
  308. with: anArray do: aBlock
  309. | old |
  310. old := elements.
  311. [ elements := anArray.
  312. aBlock value ] ensure: [ elements := old ]
  313. ! !
  314. !TrappedPathStack methodsFor: 'initialization'!
  315. initialize
  316. super initialize.
  317. elements := #().
  318. ! !
  319. TrappedPosition subclass: #TrappedSnapshot
  320. instanceVariableNames: 'prefix'
  321. package: 'Trapped-Frontend'!
  322. !TrappedSnapshot methodsFor: 'accessing'!
  323. prefix
  324. ^ prefix
  325. !
  326. prefix: anObject
  327. prefix := anObject
  328. ! !
  329. !TrappedSnapshot methodsFor: 'action'!
  330. do: aBlock
  331. TrappedPathStack current with: {prefix}, path do: [ aBlock value: model ]
  332. ! !
  333. !Array methodsFor: '*Trapped-Frontend'!
  334. asTrapProcSendTo: anObject
  335. | selector args |
  336. selector := ''.
  337. args := #().
  338. self withIndexDo: [ :element :index | index odd
  339. ifTrue: [ selector := selector, element ]
  340. ifFalse: [ selector := selector, ':'. args add: element ] ].
  341. ^anObject perform: selector withArguments: args
  342. !
  343. trapDescend: aBlock
  344. Trapped current descend: self snapshotDo: aBlock
  345. ! !
  346. !HTMLCanvas methodsFor: '*Trapped-Frontend'!
  347. trapIter: path do: aBlock
  348. self with: [ :html | (html tag: 'script') at: 'type' put: 'application/x-beacon'; trapIter: path after: aBlock ]
  349. ! !
  350. !Object methodsFor: '*Trapped-Frontend'!
  351. asTrapProcSendTo: anObject
  352. self error: 'Trapped cannot use processor descriptor of ', self class name, ' type.'
  353. ! !
  354. !String methodsFor: '*Trapped-Frontend'!
  355. asTrapProcSendTo: anObject
  356. ^anObject perform: self
  357. ! !
  358. !TagBrush methodsFor: '*Trapped-Frontend'!
  359. trap: path
  360. self trap: path processors: #()
  361. !
  362. trap: path processors: anArray
  363. path trapDescend: [ :snap |
  364. (TrappedProcessingChain newFromProcessorSpecs: anArray)
  365. forSnapshot: snap andBrush: self ]
  366. !
  367. trap: path read: aBlock
  368. path trapDescend: [ :snap |
  369. snap watch: [ :data |
  370. (self asJQuery closest: 'html') toArray isEmpty ifTrue: [ AxonOff signal ].
  371. snap do: [ self with: [ :html | aBlock value: data value: html ] ]
  372. ]
  373. ]
  374. !
  375. trapGuard: anArray contents: aBlock
  376. #() trapDescend: [ :snap |
  377. | shown |
  378. shown := nil.
  379. self trap: anArray read: [ :gdata |
  380. | sanitized |
  381. sanitized := gdata ifNil: [ false ].
  382. shown = sanitized ifFalse: [
  383. shown := sanitized.
  384. shown
  385. ifTrue: [ snap do: [ self contents: aBlock ]. self asJQuery show ]
  386. ifFalse: [ self asJQuery hide; empty ] ] ] ]
  387. !
  388. trapIter: path after: aBlock
  389. | end |
  390. end := TagBrush fromJQuery: ('<script type="application/x-beacon" />' asJQuery) canvas: canvas.
  391. self element parentNode insertBefore: end element reference: self element nextSibling.
  392. self trap: path read: [ :model |
  393. Trapped loop: model between: self and: end do: aBlock.
  394. ]
  395. ! !