Trapped-Frontend.st 16 KB

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