Trapped-Frontend.st 12 KB

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