Trapped-Frontend.st 15 KB

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