Trapped-Frontend.st 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  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: aDataChain
  7. chain := aDataChain
  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: 'action'!
  22. modifyTarget
  23. self target modify: [ self value ]
  24. !
  25. toTargetAttr: aString
  26. self target asJQuery attr: aString put: (self value ifNotNil: [ :o | o value ] ifNil: [[]])
  27. !
  28. toTargetContents
  29. self target contents: self value
  30. !
  31. toTargetValue
  32. self target asJQuery val: (self value ifNotNil: [ :o | o value ] ifNil: [[]])
  33. ! !
  34. !TrappedDataCarrier class methodsFor: 'not yet classified'!
  35. on: aDataChain target: anObject
  36. ^self new
  37. chain: aDataChain;
  38. target: anObject;
  39. yourself
  40. ! !
  41. TrappedDataCarrier subclass: #TrappedDataCarrierToModel
  42. instanceVariableNames: 'index'
  43. package: 'Trapped-Frontend'!
  44. !TrappedDataCarrierToModel methodsFor: 'not yet classified'!
  45. proceed
  46. index := index ifNil: [ chain lastProcessorNo ] ifNotNil: [ index - 1 ].
  47. (chain processorNo: index) toModel: self
  48. ! !
  49. TrappedDataCarrier subclass: #TrappedDataCarrierToView
  50. instanceVariableNames: 'index'
  51. package: 'Trapped-Frontend'!
  52. !TrappedDataCarrierToView methodsFor: 'not yet classified'!
  53. proceed
  54. index := index ifNil: [ chain firstProcessorNo ] ifNotNil: [ index + 1 ].
  55. (chain processorNo: index) toView: self
  56. ! !
  57. Object subclass: #TrappedDataChain
  58. instanceVariableNames: 'processors'
  59. package: 'Trapped-Frontend'!
  60. !TrappedDataChain methodsFor: 'accessing'!
  61. firstProcessorNo
  62. ^1
  63. !
  64. lastProcessorNo
  65. ^processors size
  66. !
  67. processorNo: aNumber
  68. ^processors at: aNumber
  69. !
  70. processors: anArray
  71. processors := anArray
  72. ! !
  73. !TrappedDataChain methodsFor: 'action'!
  74. forSnapshot: aSnapshot andBrush: aTagBrush
  75. | toViewCarrier toModelCarrier |
  76. toViewCarrier := TrappedDataCarrierToView on: self target: aTagBrush.
  77. toModelCarrier := TrappedDataCarrierToModel on: self target: aSnapshot.
  78. processors do: [ :each | each installToView: toViewCarrier toModel: toModelCarrier ]
  79. ! !
  80. !TrappedDataChain class methodsFor: 'instance creation'!
  81. new: anArray
  82. ^self new
  83. processors: { self blackboardReaderWriter }, anArray;
  84. yourself
  85. !
  86. newFromProcessorSpecs: anArray
  87. ^self new: (anArray collect: [ :each | each isString
  88. ifTrue: [ TrappedProcessor perform: each ]
  89. ifFalse: [
  90. | selector args |
  91. selector := ''.
  92. args := #().
  93. each withIndexDo: [ :element :index | index odd
  94. ifTrue: [ selector := selector, element ]
  95. ifFalse: [ selector := selector, ':'. args add: element ] ].
  96. TrappedProcessor perform: selector withArguments: args ] ])
  97. ! !
  98. !TrappedDataChain class methodsFor: 'private'!
  99. blackboardReaderWriter
  100. ^TrappedProcessorBlackboard new
  101. ! !
  102. Widget subclass: #TrappedDumbView
  103. instanceVariableNames: ''
  104. package: 'Trapped-Frontend'!
  105. !TrappedDumbView commentStamp!
  106. I just read and show an actual path.!
  107. !TrappedDumbView methodsFor: 'rendering'!
  108. renderOn: html
  109. html root trap: #()
  110. ! !
  111. Object subclass: #TrappedProcessor
  112. instanceVariableNames: ''
  113. package: 'Trapped-Frontend'!
  114. !TrappedProcessor commentStamp!
  115. I process data in TrappedDataChain.
  116. I am stateless flyweight (aka servant)
  117. and will get all necessary data as arguments in API calls.
  118. My public API is:
  119. - installToView:toModel:
  120. This gets two TrappedDataCarriers set up without actual data
  121. and at the beginning of their chains. It should do one-time
  122. installation task needed (install event handlers etc.).
  123. To start a chain, do: dataCarrier copy value: data; proceed.
  124. - toView:
  125. This performs transformation of TrappedDataCarrier on its way from model to view.
  126. Should call aDataCarrier proceed to proceed to subsequent step.
  127. - toModel:
  128. This performs transformation of TrappedDataToken on its way from view to model.
  129. Should call aDataCarrier proceed to proceed to subsequent step.!
  130. !TrappedProcessor methodsFor: 'data transformation'!
  131. toModel: aDataCarrier
  132. "by default, proceed"
  133. aDataCarrier proceed
  134. !
  135. toView: aDataCarrier
  136. "by default, proceed"
  137. aDataCarrier proceed
  138. ! !
  139. !TrappedProcessor methodsFor: 'installation'!
  140. installToView: aDataCarrier toModel: anotherDataCarrier
  141. "by default, do nothing"
  142. ! !
  143. !TrappedProcessor class methodsFor: 'factory'!
  144. contents
  145. ^TrappedProcessorContents new
  146. !
  147. inputChecked
  148. ^TrappedProcessorInputChecked new
  149. !
  150. inputValue
  151. ^TrappedProcessorInputValue new
  152. !
  153. whenClicked
  154. ^TrappedProcessorWhenClicked new
  155. !
  156. whenSubmitted
  157. ^TrappedProcessorWhenSubmitted new
  158. !
  159. widget: aString
  160. ^TrappedProcessorWidget new: aString
  161. ! !
  162. TrappedProcessor subclass: #TrappedProcessorBlackboard
  163. instanceVariableNames: ''
  164. package: 'Trapped-Frontend'!
  165. !TrappedProcessorBlackboard commentStamp!
  166. I am used internally to fetch data from blackboard
  167. or write it back.!
  168. !TrappedProcessorBlackboard methodsFor: 'data transformation'!
  169. toModel: aDataCarrier
  170. aDataCarrier modifyTarget
  171. ! !
  172. !TrappedProcessorBlackboard methodsFor: 'installation'!
  173. installToView: aDataCarrier toModel: anotherDataCarrier
  174. | snap |
  175. snap := anotherDataCarrier target.
  176. snap watch: [ :data |
  177. (aDataCarrier target asJQuery closest: 'html') toArray isEmpty ifTrue: [ KeyedPubSubUnsubscribe signal ].
  178. snap do: [ aDataCarrier copy value: data; proceed ] ]
  179. ! !
  180. TrappedProcessor subclass: #TrappedProcessorContents
  181. instanceVariableNames: ''
  182. package: 'Trapped-Frontend'!
  183. !TrappedProcessorContents commentStamp!
  184. I put data into target via contents: in toView:!
  185. !TrappedProcessorContents methodsFor: 'data transformation'!
  186. toView: aDataCarrier
  187. aDataCarrier toTargetContents
  188. ! !
  189. TrappedProcessor subclass: #TrappedProcessorInputChecked
  190. instanceVariableNames: ''
  191. package: 'Trapped-Frontend'!
  192. !TrappedProcessorInputChecked commentStamp!
  193. I bind to checkbox checked attribute.!
  194. !TrappedProcessorInputChecked methodsFor: 'data transformation'!
  195. toView: aDataCarrier
  196. aDataCarrier toTargetAttr: 'checked'
  197. ! !
  198. !TrappedProcessorInputChecked methodsFor: 'installation'!
  199. installToView: aDataCarrier toModel: anotherDataCarrier
  200. | brush |
  201. brush := aDataCarrier target.
  202. brush onChange: [ anotherDataCarrier copy value: (brush asJQuery attr: 'checked') notNil; proceed ]
  203. ! !
  204. TrappedProcessor subclass: #TrappedProcessorInputValue
  205. instanceVariableNames: ''
  206. package: 'Trapped-Frontend'!
  207. !TrappedProcessorInputValue commentStamp!
  208. I bind to input value.!
  209. !TrappedProcessorInputValue methodsFor: 'data transformation'!
  210. toView: aDataCarrier
  211. aDataCarrier toTargetValue
  212. ! !
  213. !TrappedProcessorInputValue methodsFor: 'installation'!
  214. installToView: aDataCarrier toModel: anotherDataCarrier
  215. | brush |
  216. brush := aDataCarrier target.
  217. brush onChange: [ anotherDataCarrier copy value: brush asJQuery val; proceed ]
  218. ! !
  219. TrappedProcessor subclass: #TrappedProcessorWhenClicked
  220. instanceVariableNames: ''
  221. package: 'Trapped-Frontend'!
  222. !TrappedProcessorWhenClicked commentStamp!
  223. I bind to an element and send true to blackboard when clicked.!
  224. !TrappedProcessorWhenClicked methodsFor: 'installation'!
  225. installToView: aDataCarrier toModel: anotherDataCarrier
  226. aDataCarrier target onClick: [ anotherDataCarrier copy value: true; proceed. false ]
  227. ! !
  228. TrappedProcessor subclass: #TrappedProcessorWhenSubmitted
  229. instanceVariableNames: ''
  230. package: 'Trapped-Frontend'!
  231. !TrappedProcessorWhenSubmitted commentStamp!
  232. I bind to a form and send true to blackboard when submitted.!
  233. !TrappedProcessorWhenSubmitted methodsFor: 'installation'!
  234. installToView: aDataCarrier toModel: anotherDataCarrier
  235. aDataCarrier target onSubmit: [ anotherDataCarrier copy value: true; proceed. false ]
  236. ! !
  237. TrappedProcessor subclass: #TrappedStoppingProcessor
  238. instanceVariableNames: ''
  239. package: 'Trapped-Frontend'!
  240. !TrappedStoppingProcessor commentStamp!
  241. I do not proceed in toView: nor in toModel:
  242. I am therefore only interesting for my side-effects from install step.!
  243. !TrappedStoppingProcessor methodsFor: 'data transformation'!
  244. toModel: aDataCarrier
  245. "stop"
  246. !
  247. toView: aDataCarrier
  248. "stop"
  249. ! !
  250. TrappedStoppingProcessor subclass: #TrappedProcessorWidget
  251. instanceVariableNames: 'viewName'
  252. package: 'Trapped-Frontend'!
  253. !TrappedProcessorWidget commentStamp!
  254. When installed, I insert a widget instance of the class specified when creating me.!
  255. !TrappedProcessorWidget methodsFor: 'accessing'!
  256. viewName: aString
  257. viewName := aString
  258. ! !
  259. !TrappedProcessorWidget methodsFor: 'installation'!
  260. installToView: aDataCarrier toModel: anotherDataCarrier
  261. anotherDataCarrier target do: [ aDataCarrier target with: (Smalltalk current at: viewName) new ]
  262. ! !
  263. !TrappedProcessorWidget class methodsFor: 'instance creation'!
  264. new: aString
  265. ^self new
  266. viewName: aString;
  267. yourself
  268. ! !
  269. Object subclass: #TrappedSingleton
  270. instanceVariableNames: ''
  271. package: 'Trapped-Frontend'!
  272. !TrappedSingleton methodsFor: 'action'!
  273. start: args
  274. ^ self subclassResponsibility
  275. ! !
  276. TrappedSingleton class instanceVariableNames: 'current'!
  277. !TrappedSingleton class methodsFor: 'accessing'!
  278. current
  279. ^ current ifNil: [ current := self new ]
  280. ! !
  281. !TrappedSingleton class methodsFor: 'action'!
  282. start: args
  283. self current start: args
  284. ! !
  285. TrappedSingleton subclass: #Trapped
  286. instanceVariableNames: 'registry'
  287. package: 'Trapped-Frontend'!
  288. !Trapped methodsFor: 'accessing'!
  289. byName: aString
  290. ^ registry at: aString
  291. !
  292. register: aListKeyedEntity
  293. self register: aListKeyedEntity name: aListKeyedEntity class name
  294. !
  295. register: aListKeyedEntity name: aString
  296. registry at: aString put: aListKeyedEntity
  297. ! !
  298. !Trapped methodsFor: 'action'!
  299. descend: anArray snapshotDo: aBlock
  300. | tpsc |
  301. tpsc := TrappedPathStack current.
  302. tpsc append: anArray do: [
  303. | path model |
  304. path := tpsc elements copy.
  305. model := self byName: path first.
  306. aBlock value: (TrappedSnapshot new path: path model: model)
  307. ]
  308. !
  309. injectToJQuery: aJQuery
  310. (aJQuery find: '[data-trap]') each: [ :index :elem |
  311. | jq parsed |
  312. jq := elem asJQuery.
  313. parsed := Trapped parse: (jq attr: 'data-trap').
  314. parsed do: [ :rule |
  315. (HTMLCanvas onJQuery: jq) root trap: rule first processors: (rule at: 2 ifAbsent: [#()]) ].
  316. jq removeAttr: 'data-trap' ]
  317. !
  318. start: args
  319. args do: [ :each | self register: each ].
  320. self injectToJQuery: 'body' asJQuery
  321. ! !
  322. !Trapped methodsFor: 'initialization'!
  323. initialize
  324. super initialize.
  325. registry := #{}.
  326. ! !
  327. !Trapped class methodsFor: 'accessing'!
  328. parse: aString
  329. ^ (aString tokenize: '.') collect: [ :rule |
  330. (rule tokenize: ':') collect: [ :message |
  331. | result stack anArray |
  332. anArray := message tokenize: ' '.
  333. result := #().
  334. stack := { result }.
  335. anArray do: [ :each |
  336. | asNum inner close |
  337. close := 0.
  338. inner := each.
  339. [ inner notEmpty and: [ inner first = '(' ]] whileTrue: [ inner := inner allButFirst. stack add: (stack last add: #()) ].
  340. [ inner notEmpty and: [ inner last = ')' ]] whileTrue: [ inner := inner allButLast. close := close + 1 ].
  341. asNum := (inner ifEmpty: [ 'NaN' ]) asNumber.
  342. asNum = asNum ifTrue: [ stack last add: asNum ] ifFalse: [
  343. inner ifNotEmpty: [ stack last add: inner ] ].
  344. close timesRepeat: [ stack removeLast ] ].
  345. result ] ]
  346. ! !
  347. !Trapped class methodsFor: 'private'!
  348. envelope: envelope loop: model before: endjq tag: aSymbol do: aBlock
  349. | envjq |
  350. envjq := envelope asJQuery.
  351. model withIndexDo: [ :item :i |
  352. envelope with: [ :html | (html perform: aSymbol) trap: {i} read: aBlock ].
  353. envjq children detach insertBefore: endjq.
  354. ].
  355. envjq remove
  356. !
  357. loop: model between: start and: end tag: aSymbol do: aBlock
  358. (start asJQuery nextUntil: end element) remove.
  359. start with: [ :html | model ifNotNil: [
  360. self envelope: html div loop: model before: end asJQuery tag: aSymbol do: aBlock
  361. ]]
  362. ! !
  363. TrappedSingleton subclass: #TrappedPathStack
  364. instanceVariableNames: 'elements'
  365. package: 'Trapped-Frontend'!
  366. !TrappedPathStack methodsFor: 'accessing'!
  367. elements
  368. ^elements
  369. ! !
  370. !TrappedPathStack methodsFor: 'descending'!
  371. append: anArray do: aBlock
  372. self with: elements, anArray do: aBlock
  373. !
  374. with: anArray do: aBlock
  375. | old |
  376. old := elements.
  377. [ elements := anArray.
  378. aBlock value ] ensure: [ elements := old ]
  379. ! !
  380. !TrappedPathStack methodsFor: 'initialization'!
  381. initialize
  382. super initialize.
  383. elements := #().
  384. ! !
  385. Object subclass: #TrappedSnapshot
  386. instanceVariableNames: 'path model'
  387. package: 'Trapped-Frontend'!
  388. !TrappedSnapshot methodsFor: 'accessing'!
  389. model
  390. ^model
  391. !
  392. path
  393. ^path
  394. !
  395. path: anArray model: aTrappedMW
  396. path := anArray.
  397. model := aTrappedMW
  398. ! !
  399. !TrappedSnapshot methodsFor: 'action'!
  400. do: aBlock
  401. TrappedPathStack current with: path do: [ aBlock value: model ]
  402. !
  403. modify: aBlock
  404. self model modify: self path allButFirst do: aBlock
  405. !
  406. watch: aBlock
  407. self model watch: self path allButFirst do: aBlock
  408. ! !
  409. !Array methodsFor: '*Trapped-Frontend'!
  410. trapDescend: aBlock
  411. Trapped current descend: self snapshotDo: aBlock
  412. ! !
  413. !HTMLCanvas methodsFor: '*Trapped-Frontend'!
  414. trapIter: path tag: aSymbol do: aBlock
  415. | start end |
  416. self with: [ :html | start := html script. end := html script ].
  417. start trap: path read: [ :model |
  418. Trapped loop: model between: start and: end tag: aSymbol do: aBlock.
  419. ]
  420. ! !
  421. !TagBrush methodsFor: '*Trapped-Frontend'!
  422. trap: path
  423. self trap: path processors: #(contents)
  424. !
  425. trap: path processors: anArray
  426. path trapDescend: [ :snap |
  427. (TrappedDataChain newFromProcessorSpecs: anArray)
  428. forSnapshot: snap andBrush: self ]
  429. !
  430. trap: path read: aBlock
  431. path trapDescend: [ :snap |
  432. snap watch: [ :data |
  433. (self asJQuery closest: 'html') toArray isEmpty ifTrue: [ KeyedPubSubUnsubscribe signal ].
  434. snap do: [ self with: [ :html | aBlock value: data value: html ] ]
  435. ]
  436. ]
  437. !
  438. trapGuard: anArray contents: aBlock
  439. #() trapDescend: [ :snap |
  440. | shown |
  441. shown := nil.
  442. self trap: anArray read: [ :gdata |
  443. | sanitized |
  444. sanitized := gdata ifNil: [ false ].
  445. shown = sanitized ifFalse: [
  446. shown := sanitized.
  447. shown
  448. ifTrue: [ snap do: [ self contents: aBlock ]. self asJQuery show ]
  449. ifFalse: [ self asJQuery hide; empty ] ] ] ]
  450. ! !