Trapped-Frontend.st 12 KB

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