Trapped-Frontend.st 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  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. guard: aString
  158. ^TrappedProcessorGuard new: aString
  159. !
  160. inputChecked
  161. ^TrappedProcessorInputChecked new
  162. !
  163. inputValue
  164. ^TrappedProcessorInputValue new
  165. !
  166. path
  167. ^TrappedProcessorDescend new
  168. !
  169. signal: aString
  170. ^TrappedProcessorSignal new: aString
  171. !
  172. whenClicked
  173. ^TrappedProcessorWhenClicked new
  174. !
  175. whenSubmitted
  176. ^TrappedProcessorWhenSubmitted new
  177. !
  178. widget: aString
  179. ^TrappedProcessorWidget new: aString
  180. ! !
  181. TrappedProcessor subclass: #TrappedDataExpectingProcessor
  182. instanceVariableNames: ''
  183. package: 'Trapped-Frontend'!
  184. !TrappedDataExpectingProcessor commentStamp!
  185. I answer true to isExpectingModelData and serve as a base class
  186. for processor that present / change model data.
  187. When at least one of my instances is present in the chain,
  188. automatic databinding processor is added at the beginning
  189. (the data-binding scenario); otherwise, the chain
  190. is run immediately with true as data (run-once scenario).!
  191. !TrappedDataExpectingProcessor methodsFor: 'testing'!
  192. isExpectingModelData
  193. ^true
  194. ! !
  195. TrappedDataExpectingProcessor subclass: #TrappedProcessorContents
  196. instanceVariableNames: ''
  197. package: 'Trapped-Frontend'!
  198. !TrappedProcessorContents commentStamp!
  199. I put data into target via contents: in toView:!
  200. !TrappedProcessorContents methodsFor: 'data transformation'!
  201. toView: aDataCarrier
  202. aDataCarrier toTargetContents
  203. ! !
  204. TrappedDataExpectingProcessor subclass: #TrappedProcessorInputChecked
  205. instanceVariableNames: ''
  206. package: 'Trapped-Frontend'!
  207. !TrappedProcessorInputChecked commentStamp!
  208. I bind to checkbox checked attribute.!
  209. !TrappedProcessorInputChecked methodsFor: 'data transformation'!
  210. toView: aDataCarrier
  211. aDataCarrier toTargetAttr: 'checked'
  212. ! !
  213. !TrappedProcessorInputChecked methodsFor: 'installation'!
  214. installToView: aDataCarrier toModel: anotherDataCarrier
  215. | brush |
  216. brush := aDataCarrier target.
  217. brush onChange: [ anotherDataCarrier copy value: (brush asJQuery attr: 'checked') notNil; proceed ]
  218. ! !
  219. TrappedDataExpectingProcessor subclass: #TrappedProcessorInputValue
  220. instanceVariableNames: ''
  221. package: 'Trapped-Frontend'!
  222. !TrappedProcessorInputValue commentStamp!
  223. I bind to input value.!
  224. !TrappedProcessorInputValue methodsFor: 'data transformation'!
  225. toView: aDataCarrier
  226. aDataCarrier toTargetValue
  227. ! !
  228. !TrappedProcessorInputValue methodsFor: 'installation'!
  229. installToView: aDataCarrier toModel: anotherDataCarrier
  230. | brush |
  231. brush := aDataCarrier target.
  232. brush onChange: [ anotherDataCarrier copy value: brush asJQuery val; proceed ]
  233. ! !
  234. TrappedProcessor subclass: #TrappedProcessorBlackboard
  235. instanceVariableNames: ''
  236. package: 'Trapped-Frontend'!
  237. !TrappedProcessorBlackboard commentStamp!
  238. I am used internally to fetch data from blackboard
  239. or write it back.
  240. I am added to the beginning of the chain
  241. when the chain contains at least one element
  242. that isExpectingModelData (see TrappedDataExpectingProcessor).!
  243. !TrappedProcessorBlackboard methodsFor: 'data transformation'!
  244. toModel: aDataCarrier
  245. aDataCarrier modifyTarget
  246. ! !
  247. !TrappedProcessorBlackboard methodsFor: 'installation'!
  248. installToView: aDataCarrier toModel: anotherDataCarrier
  249. | snap |
  250. snap := anotherDataCarrier target.
  251. snap watch: [ :data |
  252. (aDataCarrier target asJQuery closest: 'html') toArray isEmpty ifTrue: [ KeyedPubSubUnsubscribe signal ].
  253. snap do: [ aDataCarrier copy value: data; proceed ] ].
  254. aDataCarrier value: false
  255. ! !
  256. TrappedProcessor subclass: #TrappedProcessorDescend
  257. instanceVariableNames: ''
  258. package: 'Trapped-Frontend'!
  259. !TrappedProcessorDescend commentStamp!
  260. I intepret data-trap in descendants of my brush.!
  261. !TrappedProcessorDescend methodsFor: 'data transformation'!
  262. toView: aDataCarrier
  263. Trapped current injectToJQuery: aDataCarrier target asJQuery children
  264. ! !
  265. TrappedProcessor subclass: #TrappedProcessorGuard
  266. instanceVariableNames: 'guardPath'
  267. package: 'Trapped-Frontend'!
  268. !TrappedProcessorGuard commentStamp!
  269. I am used to guard contants of the brush I am installed on.
  270. I observe guard expression in the model,
  271. and when it changes to nil or false, I delete the brush contents;
  272. on the other hand, when it changes to non-nil and non-false,
  273. I run the rest on the chain, which should be one-time
  274. that sets up the contents,!
  275. !TrappedProcessorGuard methodsFor: 'accessing'!
  276. guardPath: anArray
  277. guardPath := anArray
  278. ! !
  279. !TrappedProcessorGuard methodsFor: 'data transformation'!
  280. toModel: aDataCarrier
  281. "stop"
  282. !
  283. toView: aDataCarrier
  284. | frozen |
  285. frozen := aDataCarrier copy.
  286. frozen target trapGuard: guardPath contents: [ frozen copy proceed ]
  287. ! !
  288. !TrappedProcessorGuard class methodsFor: 'instance creation'!
  289. new: anArray
  290. ^ self new
  291. guardPath: anArray;
  292. yourself
  293. ! !
  294. TrappedProcessor subclass: #TrappedProcessorSignal
  295. instanceVariableNames: 'selector'
  296. package: 'Trapped-Frontend'!
  297. !TrappedProcessorSignal commentStamp!
  298. Instead of writing data directly to model,
  299. I instead modify it by sending a message specified when instantiating me.!
  300. !TrappedProcessorSignal methodsFor: 'accessing'!
  301. selector: aString
  302. selector := aString
  303. ! !
  304. !TrappedProcessorSignal methodsFor: 'data transformation'!
  305. toModel: aDataCarrier
  306. aDataCarrier modifyTargetByPerforming: selector
  307. !
  308. toView: aDataCarrier
  309. "stop"
  310. ! !
  311. !TrappedProcessorSignal class methodsFor: 'instance creation'!
  312. new: aString
  313. ^self new
  314. selector: aString;
  315. yourself
  316. ! !
  317. TrappedProcessor subclass: #TrappedProcessorTerminator
  318. instanceVariableNames: ''
  319. package: 'Trapped-Frontend'!
  320. !TrappedProcessorTerminator commentStamp!
  321. I do not proceed in toView:.
  322. I am added automatically to end of chain when it does not contain
  323. any element that isExpectingModelData (see TrappedDataExpectingProcessor).!
  324. !TrappedProcessorTerminator methodsFor: 'data transformation'!
  325. toView: aDataCarrier
  326. "stop"
  327. ! !
  328. TrappedProcessor subclass: #TrappedProcessorWhenClicked
  329. instanceVariableNames: ''
  330. package: 'Trapped-Frontend'!
  331. !TrappedProcessorWhenClicked commentStamp!
  332. I bind to an element and send true to blackboard when clicked.!
  333. !TrappedProcessorWhenClicked methodsFor: 'installation'!
  334. installToView: aDataCarrier toModel: anotherDataCarrier
  335. aDataCarrier target onClick: [ anotherDataCarrier copy proceed. false ]
  336. ! !
  337. TrappedProcessor subclass: #TrappedProcessorWhenSubmitted
  338. instanceVariableNames: ''
  339. package: 'Trapped-Frontend'!
  340. !TrappedProcessorWhenSubmitted commentStamp!
  341. I bind to a form and send true to blackboard when submitted.!
  342. !TrappedProcessorWhenSubmitted methodsFor: 'installation'!
  343. installToView: aDataCarrier toModel: anotherDataCarrier
  344. aDataCarrier target onSubmit: [ anotherDataCarrier copy proceed. false ]
  345. ! !
  346. TrappedProcessor subclass: #TrappedProcessorWidget
  347. instanceVariableNames: 'viewName'
  348. package: 'Trapped-Frontend'!
  349. !TrappedProcessorWidget commentStamp!
  350. I insert a widget instance of the class specified when creating me.!
  351. !TrappedProcessorWidget methodsFor: 'accessing'!
  352. viewName: aString
  353. viewName := aString
  354. ! !
  355. !TrappedProcessorWidget methodsFor: 'data transformation'!
  356. toView: aDataCarrier
  357. aDataCarrier target with: (Smalltalk current at: viewName) new
  358. ! !
  359. !TrappedProcessorWidget class methodsFor: 'instance creation'!
  360. new: aString
  361. ^self new
  362. viewName: aString;
  363. yourself
  364. ! !
  365. Object subclass: #TrappedSingleton
  366. instanceVariableNames: ''
  367. package: 'Trapped-Frontend'!
  368. !TrappedSingleton methodsFor: 'action'!
  369. start: args
  370. ^ self subclassResponsibility
  371. ! !
  372. TrappedSingleton class instanceVariableNames: 'current'!
  373. !TrappedSingleton class methodsFor: 'accessing'!
  374. current
  375. ^ current ifNil: [ current := self new ]
  376. ! !
  377. !TrappedSingleton class methodsFor: 'action'!
  378. start: args
  379. self current start: args
  380. ! !
  381. TrappedSingleton subclass: #Trapped
  382. instanceVariableNames: 'registry'
  383. package: 'Trapped-Frontend'!
  384. !Trapped methodsFor: 'accessing'!
  385. byName: aString
  386. ^ registry at: aString
  387. !
  388. register: aListKeyedEntity
  389. self register: aListKeyedEntity name: aListKeyedEntity class name
  390. !
  391. register: aListKeyedEntity name: aString
  392. registry at: aString put: aListKeyedEntity
  393. ! !
  394. !Trapped methodsFor: 'action'!
  395. descend: anArray snapshotDo: aBlock
  396. | tpsc |
  397. tpsc := TrappedPathStack current.
  398. tpsc append: anArray do: [
  399. | path model |
  400. path := tpsc elements copy.
  401. model := self byName: path first.
  402. aBlock value: (TrappedSnapshot new path: path model: model)
  403. ]
  404. !
  405. injectToJQuery: aJQuery
  406. aJQuery each: [ :index :elem |
  407. | jq |
  408. jq := elem asJQuery.
  409. (jq is: '[data-trap]')
  410. ifTrue: [
  411. | parsed |
  412. parsed := Trapped parse: (jq attr: 'data-trap').
  413. jq removeAttr: 'data-trap'.
  414. parsed do: [ :rule |
  415. (HTMLCanvas onJQuery: jq) root trap: rule first processors: (rule at: 2 ifAbsent: [#()]) ] ]
  416. ifFalse: [ self injectToJQuery: jq children ] ]
  417. !
  418. start: args
  419. args do: [ :each | self register: each ].
  420. self injectToJQuery: 'html' asJQuery
  421. ! !
  422. !Trapped methodsFor: 'initialization'!
  423. initialize
  424. super initialize.
  425. registry := #{}.
  426. ! !
  427. !Trapped class methodsFor: 'accessing'!
  428. parse: aString
  429. ^ (aString tokenize: '.') collect: [ :rule |
  430. (rule tokenize: ':') collect: [ :message |
  431. | result stack anArray |
  432. anArray := message tokenize: ' '.
  433. result := #().
  434. stack := { result }.
  435. anArray do: [ :each |
  436. | asNum inner close |
  437. close := 0.
  438. inner := each.
  439. [ inner notEmpty and: [ inner first = '(' ]] whileTrue: [ inner := inner allButFirst. stack add: (stack last add: #()) ].
  440. [ inner notEmpty and: [ inner last = ')' ]] whileTrue: [ inner := inner allButLast. close := close + 1 ].
  441. asNum := (inner ifEmpty: [ 'NaN' ]) asNumber.
  442. asNum = asNum ifTrue: [ stack last add: asNum ] ifFalse: [
  443. inner ifNotEmpty: [ stack last add: inner ] ].
  444. close timesRepeat: [ stack removeLast ] ].
  445. result ] ]
  446. ! !
  447. !Trapped class methodsFor: 'private'!
  448. envelope: envelope loop: model before: endjq tag: aSymbol do: aBlock
  449. | envjq |
  450. envjq := envelope asJQuery.
  451. model withIndexDo: [ :item :i |
  452. envelope with: [ :html | (html perform: aSymbol) trap: {i} read: aBlock ].
  453. envjq children detach insertBefore: endjq.
  454. ].
  455. envjq remove
  456. !
  457. loop: model between: start and: end tag: aSymbol do: aBlock
  458. (start asJQuery nextUntil: end element) remove.
  459. start with: [ :html | model ifNotNil: [
  460. self envelope: html div loop: model before: end asJQuery tag: aSymbol do: aBlock
  461. ]]
  462. ! !
  463. TrappedSingleton subclass: #TrappedPathStack
  464. instanceVariableNames: 'elements'
  465. package: 'Trapped-Frontend'!
  466. !TrappedPathStack methodsFor: 'accessing'!
  467. elements
  468. ^elements
  469. ! !
  470. !TrappedPathStack methodsFor: 'descending'!
  471. append: anArray do: aBlock
  472. self with: elements, anArray do: aBlock
  473. !
  474. with: anArray do: aBlock
  475. | old |
  476. old := elements.
  477. [ elements := anArray.
  478. aBlock value ] ensure: [ elements := old ]
  479. ! !
  480. !TrappedPathStack methodsFor: 'initialization'!
  481. initialize
  482. super initialize.
  483. elements := #().
  484. ! !
  485. Object subclass: #TrappedSnapshot
  486. instanceVariableNames: 'path model'
  487. package: 'Trapped-Frontend'!
  488. !TrappedSnapshot methodsFor: 'accessing'!
  489. model
  490. ^model
  491. !
  492. path
  493. ^path
  494. !
  495. path: anArray model: aTrappedMW
  496. path := anArray.
  497. model := aTrappedMW
  498. ! !
  499. !TrappedSnapshot methodsFor: 'action'!
  500. do: aBlock
  501. TrappedPathStack current with: path do: [ aBlock value: model ]
  502. !
  503. modify: aBlock
  504. self model modify: self path allButFirst do: aBlock
  505. !
  506. watch: aBlock
  507. self model watch: self path allButFirst do: aBlock
  508. ! !
  509. !Array methodsFor: '*Trapped-Frontend'!
  510. trapDescend: aBlock
  511. Trapped current descend: self snapshotDo: aBlock
  512. ! !
  513. !HTMLCanvas methodsFor: '*Trapped-Frontend'!
  514. trapIter: path tag: aSymbol do: aBlock
  515. | start end |
  516. self with: [ :html | start := html script. end := html script ].
  517. start trap: path read: [ :model |
  518. Trapped loop: model between: start and: end tag: aSymbol do: aBlock.
  519. ]
  520. ! !
  521. !TagBrush methodsFor: '*Trapped-Frontend'!
  522. trap: path
  523. self trap: path processors: #()
  524. !
  525. trap: path processors: anArray
  526. path trapDescend: [ :snap |
  527. (TrappedProcessingChain newFromProcessorSpecs: anArray)
  528. forSnapshot: snap andBrush: self ]
  529. !
  530. trap: path read: aBlock
  531. path trapDescend: [ :snap |
  532. snap watch: [ :data |
  533. (self asJQuery closest: 'html') toArray isEmpty ifTrue: [ KeyedPubSubUnsubscribe signal ].
  534. snap do: [ self with: [ :html | aBlock value: data value: html ] ]
  535. ]
  536. ]
  537. !
  538. trapGuard: anArray contents: aBlock
  539. #() trapDescend: [ :snap |
  540. | shown |
  541. shown := nil.
  542. self trap: anArray read: [ :gdata |
  543. | sanitized |
  544. sanitized := gdata ifNil: [ false ].
  545. shown = sanitized ifFalse: [
  546. shown := sanitized.
  547. shown
  548. ifTrue: [ snap do: [ self contents: aBlock ]. self asJQuery show ]
  549. ifFalse: [ self asJQuery hide; empty ] ] ] ]
  550. ! !