editor.go 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
  1. package pagebuilder
  2. import (
  3. "database/sql"
  4. "encoding/json"
  5. "fmt"
  6. "net/url"
  7. "os"
  8. "path"
  9. "strconv"
  10. "strings"
  11. "github.com/iancoleman/strcase"
  12. "github.com/jinzhu/inflection"
  13. "github.com/qor5/admin/l10n"
  14. "github.com/qor5/admin/presets"
  15. "github.com/qor5/admin/presets/actions"
  16. "github.com/qor5/admin/publish"
  17. . "github.com/qor5/ui/vuetify"
  18. vx "github.com/qor5/ui/vuetifyx"
  19. "github.com/qor5/web"
  20. "github.com/qor5/x/i18n"
  21. "github.com/sunfmin/reflectutils"
  22. h "github.com/theplant/htmlgo"
  23. "goji.io/pat"
  24. "gorm.io/gorm"
  25. )
  26. const (
  27. AddContainerDialogEvent = "page_builder_AddContainerDialogEvent"
  28. AddContainerEvent = "page_builder_AddContainerEvent"
  29. DeleteContainerConfirmationEvent = "page_builder_DeleteContainerConfirmationEvent"
  30. DeleteContainerEvent = "page_builder_DeleteContainerEvent"
  31. MoveContainerEvent = "page_builder_MoveContainerEvent"
  32. ToggleContainerVisibilityEvent = "page_builder_ToggleContainerVisibilityEvent"
  33. MarkAsSharedContainerEvent = "page_builder_MarkAsSharedContainerEvent"
  34. RenameContainerDialogEvent = "page_builder_RenameContainerDialogEvent"
  35. RenameContainerEvent = "page_builder_RenameContainerEvent"
  36. paramPageID = "pageID"
  37. paramPageVersion = "pageVersion"
  38. paramLocale = "locale"
  39. paramContainerID = "containerID"
  40. paramMoveResult = "moveResult"
  41. paramContainerName = "containerName"
  42. paramSharedContainer = "sharedContainer"
  43. paramModelID = "modelID"
  44. DevicePhone = "phone"
  45. DeviceTablet = "tablet"
  46. DeviceComputer = "computer"
  47. )
  48. func (b *Builder) Preview(ctx *web.EventContext) (r web.PageResponse, err error) {
  49. isTpl := ctx.R.FormValue("tpl") != ""
  50. id := ctx.R.FormValue("id")
  51. version := ctx.R.FormValue("version")
  52. locale := ctx.R.FormValue("locale")
  53. var p *Page
  54. r.Body, p, err = b.renderPageOrTemplate(ctx, isTpl, id, version, locale, false)
  55. if err != nil {
  56. return
  57. }
  58. r.PageTitle = p.Title
  59. return
  60. }
  61. const editorPreviewContentPortal = "editorPreviewContentPortal"
  62. func (b *Builder) Editor(ctx *web.EventContext) (r web.PageResponse, err error) {
  63. isTpl := ctx.R.FormValue("tpl") != ""
  64. id := pat.Param(ctx.R, "id")
  65. version := ctx.R.FormValue("version")
  66. locale := ctx.R.Form.Get("locale")
  67. isLocalizable := ctx.R.Form.Has("locale")
  68. var body h.HTMLComponent
  69. var containerList h.HTMLComponent
  70. var device string
  71. var p *Page
  72. var previewHref string
  73. deviceQueries := url.Values{}
  74. if isTpl {
  75. previewHref = fmt.Sprintf("/preview?id=%s&tpl=1", id)
  76. deviceQueries.Add("tpl", "1")
  77. if isLocalizable && l10nON {
  78. previewHref = fmt.Sprintf("/preview?id=%s&tpl=1&locale=%s", id, locale)
  79. deviceQueries.Add("locale", locale)
  80. }
  81. } else {
  82. previewHref = fmt.Sprintf("/preview?id=%s&version=%s", id, version)
  83. deviceQueries.Add("version", version)
  84. if isLocalizable && l10nON {
  85. previewHref = fmt.Sprintf("/preview?id=%s&version=%s&locale=%s", id, version, locale)
  86. deviceQueries.Add("locale", locale)
  87. }
  88. }
  89. body, p, err = b.renderPageOrTemplate(ctx, isTpl, id, version, locale, true)
  90. if err != nil {
  91. return
  92. }
  93. r.PageTitle = fmt.Sprintf("Editor for %s: %s", id, p.Title)
  94. device, _ = b.getDevice(ctx)
  95. containerList, err = b.renderContainersList(ctx, p.ID, p.GetVersion(), p.GetLocale(), p.GetStatus() != publish.StatusDraft)
  96. if err != nil {
  97. return
  98. }
  99. msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
  100. r.Body = h.Components(
  101. VAppBar(
  102. VSpacer(),
  103. VBtn("").Icon(true).Children(
  104. VIcon("phone_iphone"),
  105. ).Attr("@click", web.Plaid().Queries(deviceQueries).Query("device", "phone").PushState(true).Go()).
  106. Class("mr-10").InputValue(device == "phone"),
  107. VBtn("").Icon(true).Children(
  108. VIcon("tablet_mac"),
  109. ).Attr("@click", web.Plaid().Queries(deviceQueries).Query("device", "tablet").PushState(true).Go()).
  110. Class("mr-10").InputValue(device == "tablet"),
  111. VBtn("").Icon(true).Children(
  112. VIcon("laptop_mac"),
  113. ).Attr("@click", web.Plaid().Queries(deviceQueries).Query("device", "laptop").PushState(true).Go()).
  114. InputValue(device == "laptop"),
  115. VSpacer(),
  116. VBtn(msgr.Preview).Text(true).Href(b.prefix+previewHref).Target("_blank"),
  117. VAppBarNavIcon().On("click.stop", "vars.navDrawer = !vars.navDrawer"),
  118. ).Dark(true).
  119. Color("primary").
  120. App(true),
  121. VMain(
  122. VContainer(web.Portal(body).Name(editorPreviewContentPortal)).
  123. Class("mt-6").
  124. Fluid(true),
  125. VNavigationDrawer(containerList).
  126. App(true).
  127. Right(true).
  128. Fixed(true).
  129. Value(true).
  130. Width(420).
  131. Attr("v-model", "vars.navDrawer").
  132. Attr(web.InitContextVars, `{navDrawer: null}`),
  133. ),
  134. )
  135. return
  136. }
  137. func (b *Builder) getDevice(ctx *web.EventContext) (device string, style string) {
  138. device = ctx.R.FormValue("device")
  139. if len(device) == 0 {
  140. device = b.defaultDevice
  141. }
  142. switch device {
  143. case DevicePhone:
  144. style = "width: 414px;"
  145. case DeviceTablet:
  146. style = "width: 768px;"
  147. // case Device_Computer:
  148. // style = "width: 1264px;"
  149. }
  150. return
  151. }
  152. const FreeStyleKey = "FreeStyle"
  153. func (b *Builder) renderPageOrTemplate(ctx *web.EventContext, isTpl bool, pageOrTemplateID string, version, locale string, isEditor bool) (r h.HTMLComponent, p *Page, err error) {
  154. if isTpl {
  155. tpl := &Template{}
  156. err = b.db.First(tpl, "id = ? and locale_code = ?", pageOrTemplateID, locale).Error
  157. if err != nil {
  158. return
  159. }
  160. p = tpl.Page()
  161. version = p.Version.Version
  162. } else {
  163. err = b.db.First(&p, "id = ? and version = ? and locale_code = ?", pageOrTemplateID, version, locale).Error
  164. if err != nil {
  165. return
  166. }
  167. }
  168. var isReadonly bool
  169. if p.GetStatus() != publish.StatusDraft && isEditor {
  170. isReadonly = true
  171. }
  172. var comps []h.HTMLComponent
  173. comps, err = b.renderContainers(ctx, p.ID, p.GetVersion(), p.GetLocale(), isEditor, isReadonly)
  174. if err != nil {
  175. return
  176. }
  177. r = h.Components(comps...)
  178. if b.pageLayoutFunc != nil {
  179. input := &PageLayoutInput{
  180. IsEditor: isEditor,
  181. IsPreview: !isEditor,
  182. Page: p,
  183. }
  184. if isEditor {
  185. input.EditorCss = append(input.EditorCss, h.RawHTML(`<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">`))
  186. input.EditorCss = append(input.EditorCss, h.Style(`
  187. .wrapper-shadow {
  188. position:absolute;
  189. width: 100%;
  190. height: 100%;
  191. z-index:9999;
  192. background: rgba(81, 193, 226, 0.25);
  193. opacity: 0;
  194. top: 0;
  195. left: 0;
  196. }
  197. .wrapper-shadow button{
  198. position:absolute;
  199. top: 0;
  200. right: 0;
  201. line-height: 1;
  202. font-size: 0;
  203. border: 2px outset #767676;
  204. cursor: pointer;
  205. }
  206. .wrapper-shadow.hover {
  207. cursor: pointer;
  208. opacity: 1;
  209. }`))
  210. input.FreeStyleBottomJs = []string{`
  211. function scrolltoCurrentContainer(event) {
  212. const current = document.querySelector("div[data-container-id='"+event.data+"']");
  213. if (!current) {
  214. return;
  215. }
  216. const hover = document.querySelector(".wrapper-shadow.hover")
  217. if (hover) {
  218. hover.classList.remove('hover');
  219. }
  220. window.parent.scroll({top: current.offsetTop, behavior: "smooth"});
  221. current.querySelector(".wrapper-shadow").classList.add('hover');
  222. }
  223. document.querySelectorAll('.wrapper-shadow').forEach(shadow => {
  224. shadow.addEventListener('mouseover', event => {
  225. document.querySelectorAll(".wrapper-shadow.hover").forEach(item => {
  226. item.classList.remove('hover');
  227. })
  228. shadow.classList.add('hover');
  229. })
  230. })
  231. window.addEventListener("message", scrolltoCurrentContainer, false);
  232. `}
  233. }
  234. if f := ctx.R.Context().Value(FreeStyleKey); f != nil {
  235. pl, ok := f.(*PageLayoutInput)
  236. if ok {
  237. input.FreeStyleCss = append(input.FreeStyleCss, pl.FreeStyleCss...)
  238. input.FreeStyleTopJs = append(input.FreeStyleTopJs, pl.FreeStyleTopJs...)
  239. input.FreeStyleBottomJs = append(input.FreeStyleBottomJs, pl.FreeStyleBottomJs...)
  240. }
  241. }
  242. if isEditor {
  243. // use newCtx to avoid inserting page head to head outside of iframe
  244. newCtx := &web.EventContext{
  245. R: ctx.R,
  246. Injector: &web.PageInjector{},
  247. }
  248. r = b.pageLayoutFunc(h.Components(comps...), input, newCtx)
  249. newCtx.Injector.HeadHTMLComponent("style", b.pageStyle, true)
  250. r = h.HTMLComponents{
  251. h.RawHTML("<!DOCTYPE html>\n"),
  252. h.Tag("html").Children(
  253. h.Head(
  254. newCtx.Injector.GetHeadHTMLComponent(),
  255. ),
  256. h.Body(
  257. h.Div(
  258. r,
  259. ).Id("app").Attr("v-cloak", true),
  260. newCtx.Injector.GetTailHTMLComponent(),
  261. ).Class("front"),
  262. ).AttrIf("lang", newCtx.Injector.GetHTMLLang(), newCtx.Injector.GetHTMLLang() != ""),
  263. }
  264. _, width := b.getDevice(ctx)
  265. iframeHeightName := "_iframeHeight"
  266. iframeHeightCookie, _ := ctx.R.Cookie(iframeHeightName)
  267. iframeValue := "1000px"
  268. if iframeHeightCookie != nil {
  269. iframeValue = iframeHeightCookie.Value
  270. }
  271. r = h.Div(
  272. h.RawHTML(fmt.Sprintf(`
  273. <iframe frameborder='0' scrolling='no' srcdoc="%s"
  274. @load='
  275. var height = $event.target.contentWindow.document.body.parentElement.offsetHeight+"px";
  276. $event.target.style.height=height;
  277. document.cookie="%s="+height;
  278. '
  279. style='width:100%%; display:block; border:none; padding:0; margin:0; height:%s;'></iframe>`,
  280. strings.ReplaceAll(
  281. h.MustString(r, ctx.R.Context()),
  282. "\"",
  283. "&quot;"),
  284. iframeHeightName,
  285. iframeValue,
  286. )),
  287. ).Class("page-builder-container mx-auto").Attr("style", width)
  288. } else {
  289. r = b.pageLayoutFunc(h.Components(comps...), input, ctx)
  290. ctx.Injector.HeadHTMLComponent("style", b.pageStyle, true)
  291. }
  292. }
  293. return
  294. }
  295. func (b *Builder) renderContainers(ctx *web.EventContext, pageID uint, pageVersion, locale string, isEditor bool, isReadonly bool) (r []h.HTMLComponent, err error) {
  296. var cons []*Container
  297. err = b.db.Order("display_order ASC").Find(&cons, "page_id = ? AND page_version = ? AND locale_code = ?", pageID, pageVersion, locale).Error
  298. if err != nil {
  299. return
  300. }
  301. cbs := b.getContainerBuilders(cons)
  302. device, _ := b.getDevice(ctx)
  303. for _, ec := range cbs {
  304. if ec.container.Hidden {
  305. continue
  306. }
  307. obj := ec.builder.NewModel()
  308. err = b.db.FirstOrCreate(obj, "id = ?", ec.container.ModelID).Error
  309. if err != nil {
  310. return
  311. }
  312. input := RenderInput{
  313. IsEditor: isEditor,
  314. IsReadonly: isReadonly,
  315. Device: device,
  316. }
  317. pure := ec.builder.renderFunc(obj, &input, ctx)
  318. r = append(r, pure)
  319. }
  320. return
  321. }
  322. type ContainerSorterItem struct {
  323. Index int `json:"index"`
  324. Label string `json:"label"`
  325. ModelName string `json:"model_name"`
  326. ModelID string `json:"model_id"`
  327. DisplayName string `json:"display_name"`
  328. ContainerID string `json:"container_id"`
  329. URL string `json:"url"`
  330. Shared bool `json:"shared"`
  331. VisibilityIcon string `json:"visibility_icon"`
  332. ParamID string `json:"param_id"`
  333. }
  334. type ContainerSorter struct {
  335. Items []ContainerSorterItem `json:"items"`
  336. }
  337. func (b *Builder) renderContainersList(ctx *web.EventContext, pageID uint, pageVersion, locale string, isReadonly bool) (r h.HTMLComponent, err error) {
  338. var cons []*Container
  339. err = b.db.Order("display_order ASC").Find(&cons, "page_id = ? AND page_version = ? AND locale_code = ?", pageID, pageVersion, locale).Error
  340. if err != nil {
  341. return
  342. }
  343. var sorterData ContainerSorter
  344. for i, c := range cons {
  345. vicon := "visibility"
  346. if c.Hidden {
  347. vicon = "visibility_off"
  348. }
  349. var displayName = i18n.T(ctx.R, presets.ModelsI18nModuleKey, c.DisplayName)
  350. sorterData.Items = append(sorterData.Items,
  351. ContainerSorterItem{
  352. Index: i,
  353. Label: displayName,
  354. ModelName: inflection.Plural(strcase.ToKebab(c.ModelName)),
  355. ModelID: strconv.Itoa(int(c.ModelID)),
  356. DisplayName: displayName,
  357. ContainerID: strconv.Itoa(int(c.ID)),
  358. URL: b.ContainerByName(c.ModelName).mb.Info().ListingHref(),
  359. Shared: c.Shared,
  360. VisibilityIcon: vicon,
  361. ParamID: c.PrimarySlug(),
  362. },
  363. )
  364. }
  365. msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
  366. r = web.Scope(
  367. VSheet(
  368. VCard(
  369. h.Tag("vx-draggable").
  370. Attr("v-model", "locals.items", "handle", ".handle", "animation", "300").
  371. Attr("@end", web.Plaid().
  372. URL(fmt.Sprintf("%s/editors", b.prefix)).
  373. EventFunc(MoveContainerEvent).
  374. FieldValue(paramMoveResult, web.Var("JSON.stringify(locals.items)")).
  375. Go()).Children(
  376. // VList(
  377. h.Div(
  378. VListItem(
  379. h.If(!isReadonly,
  380. VListItemIcon(VBtn("").Icon(true).Children(VIcon("drag_indicator"))).Class("handle my-2 ml-1 mr-1"),
  381. ).Else(
  382. VListItemIcon().Class("my-2 ml-1 mr-1"),
  383. ),
  384. VListItemContent(
  385. VListItemTitle(h.Text("{{item.label}}")).Attr(":style", "[item.shared ? {'color':'green'}:{}]"),
  386. ),
  387. h.If(!isReadonly,
  388. VListItemIcon(VBtn("").Icon(true).Children(VIcon("edit").Small(true))).Attr("@click",
  389. web.Plaid().
  390. URL(web.Var("item.url")).
  391. EventFunc(actions.Edit).
  392. Query(presets.ParamOverlay, actions.Dialog).
  393. Query(presets.ParamID, web.Var("item.model_id")).
  394. Go(),
  395. ).Class("my-2"),
  396. VListItemIcon(VBtn("").Icon(true).Children(VIcon("{{item.visibility_icon}}").Small(true))).Attr("@click",
  397. web.Plaid().
  398. URL(web.Var("item.url")).
  399. EventFunc(ToggleContainerVisibilityEvent).
  400. Query(paramContainerID, web.Var("item.param_id")).
  401. Go(),
  402. ).Class("my-2"),
  403. ),
  404. h.If(!isReadonly,
  405. VMenu(
  406. web.Slot(
  407. VBtn("").Children(
  408. VIcon("more_horiz"),
  409. ).Attr("v-on", "on").Text(true).Fab(true).Small(true),
  410. ).Name("activator").Scope("{ on }"),
  411. VList(
  412. VListItem(
  413. VListItemIcon(VIcon("edit_note")).Class("pl-0 mr-2"),
  414. VListItemTitle(h.Text("Rename")),
  415. ).Attr("@click",
  416. web.Plaid().
  417. URL(web.Var("item.url")).
  418. EventFunc(RenameContainerDialogEvent).
  419. Query(paramContainerID, web.Var("item.param_id")).
  420. Query(paramContainerName, web.Var("item.display_name")).
  421. Query(presets.ParamOverlay, actions.Dialog).
  422. Go(),
  423. ),
  424. VListItem(
  425. VListItemIcon(VIcon("delete")).Class("pl-0 mr-2"),
  426. VListItemTitle(h.Text("Delete")),
  427. ).Attr("@click", web.Plaid().
  428. URL(web.Var("item.url")).
  429. EventFunc(DeleteContainerConfirmationEvent).
  430. Query(paramContainerID, web.Var("item.param_id")).
  431. Query(paramContainerName, web.Var("item.display_name")).
  432. Go(),
  433. ),
  434. VListItem(
  435. VListItemIcon(VIcon("share")).Class("pl-1 mr-2"),
  436. VListItemTitle(h.Text("Mark As Shared Container")),
  437. ).Attr("@click",
  438. web.Plaid().
  439. URL(web.Var("item.url")).
  440. EventFunc(MarkAsSharedContainerEvent).
  441. Query(paramContainerID, web.Var("item.param_id")).
  442. Go(),
  443. ).Attr("v-if", "!item.shared"),
  444. ).Dense(true),
  445. ).Left(true),
  446. ),
  447. ).Class("pl-0").Attr("@click", fmt.Sprintf(`document.querySelector("iframe").contentWindow.postMessage(%s+"_"+%s,"*");`, web.Var("item.model_name"), web.Var("item.model_id"))),
  448. VDivider().Attr("v-if", "index < locals.items.length "),
  449. ).Attr("v-for", "(item, index) in locals.items", ":key", "item.index"),
  450. h.If(!isReadonly,
  451. VListItem(
  452. VListItemIcon(VIcon("add").Color("primary")).Class("ma-4"),
  453. VListItemTitle(VBtn(msgr.AddContainers).Color("primary").Text(true)),
  454. ).Attr("@click",
  455. web.Plaid().
  456. URL(fmt.Sprintf("%s/editors/%d?version=%s&locale=%s", b.prefix, pageID, pageVersion, locale)).
  457. EventFunc(AddContainerDialogEvent).
  458. Query(paramPageID, pageID).
  459. Query(paramPageVersion, pageVersion).
  460. Query(paramLocale, locale).
  461. Query(presets.ParamOverlay, actions.Dialog).
  462. Go(),
  463. ),
  464. ),
  465. // ).Class("py-0"),
  466. ),
  467. ).Outlined(true),
  468. ).Class("pa-4 pt-2"),
  469. ).Init(h.JSONString(sorterData)).VSlot("{ locals }")
  470. return
  471. }
  472. func (b *Builder) AddContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  473. pageID := ctx.QueryAsInt(paramPageID)
  474. pageVersion := ctx.R.FormValue(paramPageVersion)
  475. locale := ctx.R.FormValue(paramLocale)
  476. containerName := ctx.R.FormValue(paramContainerName)
  477. sharedContainer := ctx.R.FormValue(paramSharedContainer)
  478. modelID := ctx.QueryAsInt(paramModelID)
  479. var newModelID uint
  480. if sharedContainer == "true" {
  481. err = b.AddSharedContainerToPage(pageID, pageVersion, locale, containerName, uint(modelID))
  482. r.PushState = web.Location(url.Values{})
  483. } else {
  484. newModelID, err = b.AddContainerToPage(pageID, pageVersion, locale, containerName)
  485. r.VarsScript = web.Plaid().
  486. URL(b.ContainerByName(containerName).mb.Info().ListingHref()).
  487. EventFunc(actions.Edit).
  488. Query(presets.ParamOverlay, actions.Dialog).
  489. Query(presets.ParamID, fmt.Sprint(newModelID)).
  490. Go()
  491. }
  492. return
  493. }
  494. func (b *Builder) MoveContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  495. moveResult := ctx.R.FormValue(paramMoveResult)
  496. var result []ContainerSorterItem
  497. err = json.Unmarshal([]byte(moveResult), &result)
  498. if err != nil {
  499. return
  500. }
  501. err = b.db.Transaction(func(tx *gorm.DB) (inerr error) {
  502. for i, r := range result {
  503. if inerr = tx.Model(&Container{}).Where("id = ?", r.ContainerID).Update("display_order", i+1).Error; inerr != nil {
  504. return
  505. }
  506. }
  507. return
  508. })
  509. r.PushState = web.Location(url.Values{})
  510. return
  511. }
  512. func (b *Builder) ToggleContainerVisibility(ctx *web.EventContext) (r web.EventResponse, err error) {
  513. var container Container
  514. paramID := ctx.R.FormValue(paramContainerID)
  515. cs := container.PrimaryColumnValuesBySlug(paramID)
  516. containerID := cs["id"]
  517. locale := cs["locale_code"]
  518. err = b.db.Exec("UPDATE page_builder_containers SET hidden = NOT(COALESCE(hidden,FALSE)) WHERE id = ? AND locale_code = ?", containerID, locale).Error
  519. r.PushState = web.Location(url.Values{})
  520. return
  521. }
  522. func (b *Builder) DeleteContainerConfirmation(ctx *web.EventContext) (r web.EventResponse, err error) {
  523. paramID := ctx.R.FormValue(paramContainerID)
  524. containerName := ctx.R.FormValue(paramContainerName)
  525. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  526. Name: presets.DeleteConfirmPortalName,
  527. Body: VDialog(
  528. VCard(
  529. VCardTitle(h.Text(fmt.Sprintf("Are you sure you want to delete %s?", containerName))),
  530. VCardActions(
  531. VSpacer(),
  532. VBtn("Cancel").
  533. Depressed(true).
  534. Class("ml-2").
  535. On("click", "vars.deleteConfirmation = false"),
  536. VBtn("Delete").
  537. Color("primary").
  538. Depressed(true).
  539. Dark(true).
  540. Attr("@click", web.Plaid().
  541. URL(fmt.Sprintf("%s/editors", b.prefix)).
  542. EventFunc(DeleteContainerEvent).
  543. Query(paramContainerID, paramID).
  544. Go()),
  545. ),
  546. ),
  547. ).MaxWidth("600px").
  548. Attr("v-model", "vars.deleteConfirmation").
  549. Attr(web.InitContextVars, `{deleteConfirmation: false}`),
  550. })
  551. r.VarsScript = "setTimeout(function(){ vars.deleteConfirmation = true }, 100)"
  552. return
  553. }
  554. func (b *Builder) DeleteContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  555. var container Container
  556. paramID := ctx.R.FormValue(paramContainerID)
  557. cs := container.PrimaryColumnValuesBySlug(paramID)
  558. containerID := cs["id"]
  559. locale := cs["locale_code"]
  560. err = b.db.Delete(&Container{}, "id = ? AND locale_code = ?", containerID, locale).Error
  561. if err != nil {
  562. return
  563. }
  564. r.PushState = web.Location(url.Values{})
  565. return
  566. }
  567. func (b *Builder) AddContainerToPage(pageID int, pageVersion, locale, containerName string) (modelID uint, err error) {
  568. model := b.ContainerByName(containerName).NewModel()
  569. var dc DemoContainer
  570. b.db.Where("model_name = ? AND locale_code = ?", containerName, locale).First(&dc)
  571. if dc.ID != 0 && dc.ModelID != 0 {
  572. b.db.Where("id = ?", dc.ModelID).First(model)
  573. reflectutils.Set(model, "ID", uint(0))
  574. }
  575. err = b.db.Create(model).Error
  576. if err != nil {
  577. return
  578. }
  579. var maxOrder sql.NullFloat64
  580. err = b.db.Model(&Container{}).Select("MAX(display_order)").Where("page_id = ? and page_version = ? and locale_code = ?", pageID, pageVersion, locale).Scan(&maxOrder).Error
  581. if err != nil {
  582. return
  583. }
  584. modelID = reflectutils.MustGet(model, "ID").(uint)
  585. err = b.db.Create(&Container{
  586. PageID: uint(pageID),
  587. PageVersion: pageVersion,
  588. ModelName: containerName,
  589. DisplayName: containerName,
  590. ModelID: modelID,
  591. DisplayOrder: maxOrder.Float64 + 1,
  592. Locale: l10n.Locale{
  593. LocaleCode: locale,
  594. },
  595. }).Error
  596. if err != nil {
  597. return
  598. }
  599. return
  600. }
  601. func (b *Builder) AddSharedContainerToPage(pageID int, pageVersion, locale, containerName string, modelID uint) (err error) {
  602. var c Container
  603. err = b.db.First(&c, "model_name = ? AND model_id = ? AND shared = true", containerName, modelID).Error
  604. if err != nil {
  605. return
  606. }
  607. var maxOrder sql.NullFloat64
  608. err = b.db.Model(&Container{}).Select("MAX(display_order)").Where("page_id = ? and page_version = ? and locale_code = ?", pageID, pageVersion, locale).Scan(&maxOrder).Error
  609. if err != nil {
  610. return
  611. }
  612. err = b.db.Create(&Container{
  613. PageID: uint(pageID),
  614. PageVersion: pageVersion,
  615. ModelName: containerName,
  616. DisplayName: c.DisplayName,
  617. ModelID: modelID,
  618. Shared: true,
  619. DisplayOrder: maxOrder.Float64 + 1,
  620. Locale: l10n.Locale{
  621. LocaleCode: locale,
  622. },
  623. }).Error
  624. if err != nil {
  625. return
  626. }
  627. return
  628. }
  629. func (b *Builder) copyContainersToNewPageVersion(db *gorm.DB, pageID int, locale, oldPageVersion, newPageVersion string) (err error) {
  630. return b.copyContainersToAnotherPage(db, pageID, oldPageVersion, locale, pageID, newPageVersion, locale)
  631. }
  632. func (b *Builder) copyContainersToAnotherPage(db *gorm.DB, pageID int, pageVersion, locale string, toPageID int, toPageVersion, toPageLocale string) (err error) {
  633. var cons []*Container
  634. err = db.Order("display_order ASC").Find(&cons, "page_id = ? AND page_version = ? AND locale_code = ?", pageID, pageVersion, locale).Error
  635. if err != nil {
  636. return
  637. }
  638. for _, c := range cons {
  639. newModelID := c.ModelID
  640. if !c.Shared {
  641. model := b.ContainerByName(c.ModelName).NewModel()
  642. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  643. return
  644. }
  645. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  646. return
  647. }
  648. if err = db.Create(model).Error; err != nil {
  649. return
  650. }
  651. newModelID = reflectutils.MustGet(model, "ID").(uint)
  652. }
  653. if err = db.Create(&Container{
  654. PageID: uint(toPageID),
  655. PageVersion: toPageVersion,
  656. ModelName: c.ModelName,
  657. DisplayName: c.DisplayName,
  658. ModelID: newModelID,
  659. DisplayOrder: c.DisplayOrder,
  660. Shared: c.Shared,
  661. Locale: l10n.Locale{
  662. LocaleCode: toPageLocale,
  663. },
  664. }).Error; err != nil {
  665. return
  666. }
  667. }
  668. return
  669. }
  670. func (b *Builder) localizeContainersToAnotherPage(db *gorm.DB, pageID int, pageVersion, locale string, toPageID int, toPageVersion, toPageLocale string) (err error) {
  671. var cons []*Container
  672. err = db.Order("display_order ASC").Find(&cons, "page_id = ? AND page_version = ? AND locale_code = ?", pageID, pageVersion, locale).Error
  673. if err != nil {
  674. return
  675. }
  676. for _, c := range cons {
  677. newModelID := c.ModelID
  678. newDisplayName := c.DisplayName
  679. if !c.Shared {
  680. model := b.ContainerByName(c.ModelName).NewModel()
  681. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  682. return
  683. }
  684. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  685. return
  686. }
  687. if err = db.Create(model).Error; err != nil {
  688. return
  689. }
  690. newModelID = reflectutils.MustGet(model, "ID").(uint)
  691. } else {
  692. var count int64
  693. var temp Container
  694. if err = db.Where("model_name = ? AND locale_code = ?", c.ModelName, toPageLocale).First(&temp).Count(&count).Error; err != nil && err != gorm.ErrRecordNotFound {
  695. return
  696. }
  697. if count == 0 {
  698. model := b.ContainerByName(c.ModelName).NewModel()
  699. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  700. return
  701. }
  702. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  703. return
  704. }
  705. if err = db.Create(model).Error; err != nil {
  706. return
  707. }
  708. newModelID = reflectutils.MustGet(model, "ID").(uint)
  709. } else {
  710. newModelID = temp.ModelID
  711. newDisplayName = temp.DisplayName
  712. }
  713. }
  714. if err = db.Create(&Container{
  715. Model: gorm.Model{ID: c.ID},
  716. PageID: uint(toPageID),
  717. PageVersion: toPageVersion,
  718. ModelName: c.ModelName,
  719. DisplayName: newDisplayName,
  720. ModelID: newModelID,
  721. DisplayOrder: c.DisplayOrder,
  722. Shared: c.Shared,
  723. Locale: l10n.Locale{
  724. LocaleCode: toPageLocale,
  725. },
  726. }).Error; err != nil {
  727. return
  728. }
  729. }
  730. return
  731. }
  732. func (b *Builder) createModelAfterLocalizeDemoContainer(db *gorm.DB, c *DemoContainer) (err error) {
  733. model := b.ContainerByName(c.ModelName).NewModel()
  734. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  735. return
  736. }
  737. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  738. return
  739. }
  740. if err = db.Create(model).Error; err != nil {
  741. return
  742. }
  743. c.ModelID = reflectutils.MustGet(model, "ID").(uint)
  744. return
  745. }
  746. func (b *Builder) MarkAsSharedContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  747. var container Container
  748. paramID := ctx.R.FormValue(paramContainerID)
  749. cs := container.PrimaryColumnValuesBySlug(paramID)
  750. containerID := cs["id"]
  751. locale := cs["locale_code"]
  752. err = b.db.Model(&Container{}).Where("id = ? AND locale_code = ?", containerID, locale).Update("shared", true).Error
  753. if err != nil {
  754. return
  755. }
  756. r.PushState = web.Location(url.Values{})
  757. return
  758. }
  759. func (b *Builder) RenameContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  760. var container Container
  761. paramID := ctx.R.FormValue(paramContainerID)
  762. cs := container.PrimaryColumnValuesBySlug(paramID)
  763. containerID := cs["id"]
  764. locale := cs["locale_code"]
  765. name := ctx.R.FormValue("DisplayName")
  766. var c Container
  767. err = b.db.First(&c, "id = ? AND locale_code = ? ", containerID, locale).Error
  768. if err != nil {
  769. return
  770. }
  771. if c.Shared {
  772. err = b.db.Model(&Container{}).Where("model_name = ? AND model_id = ? AND locale_code = ?", c.ModelName, c.ModelID, locale).Update("display_name", name).Error
  773. if err != nil {
  774. return
  775. }
  776. } else {
  777. err = b.db.Model(&Container{}).Where("id = ? AND locale_code = ?", containerID, locale).Update("display_name", name).Error
  778. if err != nil {
  779. return
  780. }
  781. }
  782. r.PushState = web.Location(url.Values{})
  783. return
  784. }
  785. func (b *Builder) RenameContainerDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  786. paramID := ctx.R.FormValue(paramContainerID)
  787. name := ctx.R.FormValue(paramContainerName)
  788. okAction := web.Plaid().
  789. URL(fmt.Sprintf("%s/editors", b.prefix)).
  790. EventFunc(RenameContainerEvent).Query(paramContainerID, paramID).Go()
  791. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  792. Name: dialogPortalName,
  793. Body: web.Scope(
  794. VDialog(
  795. VCard(
  796. VCardTitle(h.Text("Rename")),
  797. VCardText(
  798. VTextField().FieldName("DisplayName").Value(name),
  799. ),
  800. VCardActions(
  801. VSpacer(),
  802. VBtn("Cancel").
  803. Depressed(true).
  804. Class("ml-2").
  805. On("click", "locals.renameDialog = false"),
  806. VBtn("OK").
  807. Color("primary").
  808. Depressed(true).
  809. Dark(true).
  810. Attr("@click", okAction),
  811. ),
  812. ),
  813. ).MaxWidth("400px").
  814. Attr("v-model", "locals.renameDialog"),
  815. ).Init("{renameDialog:true}").VSlot("{locals}"),
  816. })
  817. return
  818. }
  819. func (b *Builder) AddContainerDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  820. pageID := ctx.QueryAsInt(paramPageID)
  821. pageVersion := ctx.R.FormValue(paramPageVersion)
  822. locale := ctx.R.FormValue(paramLocale)
  823. // okAction := web.Plaid().EventFunc(RenameContainerEvent).Query(paramContainerID, containerID).Go()
  824. msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
  825. var containers []h.HTMLComponent
  826. for _, builder := range b.containerBuilders {
  827. cover := builder.cover
  828. if cover == "" {
  829. cover = path.Join(b.prefix, b.imagesPrefix, strings.ReplaceAll(builder.name, " ", "")+".png")
  830. }
  831. containers = append(containers,
  832. VCol(
  833. VCard(
  834. VImg().Src(cover).Height(200),
  835. VCardActions(
  836. VCardTitle(h.Text(i18n.T(ctx.R, presets.ModelsI18nModuleKey, builder.name))),
  837. VSpacer(),
  838. VBtn(msgr.Select).
  839. Text(true).
  840. Color("primary").Attr("@click",
  841. "locals.addContainerDialog = false;"+web.Plaid().
  842. URL(fmt.Sprintf("%s/editors/%d?version=%s&locale=%s", b.prefix, pageID, pageVersion, locale)).
  843. EventFunc(AddContainerEvent).
  844. Query(paramPageID, pageID).
  845. Query(paramPageVersion, pageVersion).
  846. Query(paramLocale, locale).
  847. Query(paramContainerName, builder.name).
  848. Go(),
  849. ),
  850. ),
  851. ),
  852. ).Cols(4),
  853. )
  854. }
  855. var cons []*Container
  856. err = b.db.Select("display_name,model_name,model_id").Where("shared = true AND locale_code = ?", locale).Group("display_name,model_name,model_id").Find(&cons).Error
  857. if err != nil {
  858. return
  859. }
  860. var sharedContainers []h.HTMLComponent
  861. for _, sharedC := range cons {
  862. c := b.ContainerByName(sharedC.ModelName)
  863. cover := c.cover
  864. if cover == "" {
  865. cover = path.Join(b.prefix, b.imagesPrefix, strings.ReplaceAll(c.name, " ", "")+".png")
  866. }
  867. sharedContainers = append(sharedContainers,
  868. VCol(
  869. VCard(
  870. VImg().Src(cover).Height(200),
  871. VCardActions(
  872. VCardTitle(h.Text(i18n.T(ctx.R, presets.ModelsI18nModuleKey, sharedC.DisplayName))),
  873. VSpacer(),
  874. VBtn(msgr.Select).
  875. Text(true).
  876. Color("primary").Attr("@click",
  877. "locals.addContainerDialog = false;"+web.Plaid().
  878. URL(fmt.Sprintf("%s/editors/%d?version=%s&locale=%s", b.prefix, pageID, pageVersion, locale)).
  879. EventFunc(AddContainerEvent).
  880. Query(paramPageID, pageID).
  881. Query(paramPageVersion, pageVersion).
  882. Query(paramLocale, locale).
  883. Query(paramContainerName, sharedC.ModelName).
  884. Query(paramModelID, sharedC.ModelID).
  885. Query(paramSharedContainer, "true").
  886. Go(),
  887. ),
  888. ),
  889. ),
  890. ).Cols(4),
  891. )
  892. }
  893. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  894. Name: dialogPortalName,
  895. Body: web.Scope(
  896. VDialog(
  897. VTabs(
  898. VTab(h.Text(msgr.New)),
  899. VTabItem(
  900. VSheet(
  901. VContainer(
  902. VRow(
  903. containers...,
  904. ),
  905. ),
  906. ),
  907. ).Attr("style", "overflow-y: scroll; overflow-x: hidden; height: 610px;"),
  908. VTab(h.Text(msgr.Shared)),
  909. VTabItem(
  910. VSheet(
  911. VContainer(
  912. VRow(
  913. sharedContainers...,
  914. ),
  915. ),
  916. ),
  917. ).Attr("style", "overflow-y: scroll; overflow-x: hidden; height: 610px;"),
  918. ),
  919. ).Width("1200px").Attr("v-model", "locals.addContainerDialog"),
  920. ).Init("{addContainerDialog:true}").VSlot("{locals}"),
  921. })
  922. return
  923. }
  924. type editorContainer struct {
  925. builder *ContainerBuilder
  926. container *Container
  927. }
  928. func (b *Builder) getContainerBuilders(cs []*Container) (r []*editorContainer) {
  929. for _, c := range cs {
  930. for _, cb := range b.containerBuilders {
  931. if cb.name == c.ModelName {
  932. r = append(r, &editorContainer{
  933. builder: cb,
  934. container: c,
  935. })
  936. }
  937. }
  938. }
  939. return
  940. }
  941. const (
  942. dialogPortalName = "pagebuilder_DialogPortalName"
  943. )
  944. func (b *Builder) pageEditorLayout(in web.PageFunc, config *presets.LayoutConfig) (out web.PageFunc) {
  945. return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
  946. ctx.Injector.HeadHTML(strings.Replace(`
  947. <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">
  948. <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
  949. <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  950. <link rel="stylesheet" href="{{prefix}}/assets/main.css">
  951. <script src='{{prefix}}/assets/vue.js'></script>
  952. <style>
  953. .page-builder-container {
  954. overflow: hidden;
  955. box-shadow: -10px 0px 13px -7px rgba(0,0,0,.3), 10px 0px 13px -7px rgba(0,0,0,.18), 5px 0px 15px 5px rgba(0,0,0,.12);
  956. }
  957. [v-cloak] {
  958. display: none;
  959. }
  960. </style>
  961. `, "{{prefix}}", b.prefix, -1))
  962. b.ps.InjectExtraAssets(ctx)
  963. if len(os.Getenv("DEV_PRESETS")) > 0 {
  964. ctx.Injector.TailHTML(`
  965. <script src='http://localhost:3080/js/chunk-vendors.js'></script>
  966. <script src='http://localhost:3080/js/app.js'></script>
  967. <script src='http://localhost:3100/js/chunk-vendors.js'></script>
  968. <script src='http://localhost:3100/js/app.js'></script>
  969. `)
  970. } else {
  971. ctx.Injector.TailHTML(strings.Replace(`
  972. <script src='{{prefix}}/assets/main.js'></script>
  973. `, "{{prefix}}", b.prefix, -1))
  974. }
  975. var innerPr web.PageResponse
  976. innerPr, err = in(ctx)
  977. if err != nil {
  978. panic(err)
  979. }
  980. action := web.POST().
  981. EventFunc(actions.Edit).
  982. URL(web.Var("\""+b.prefix+"/\"+arr[0]")).
  983. Query(presets.ParamOverlay, actions.Dialog).
  984. Query(presets.ParamID, web.Var("arr[1]")).
  985. // Query(presets.ParamOverlayAfterUpdateScript,
  986. // web.Var(
  987. // h.JSONString(web.POST().
  988. // PushState(web.Location(url.Values{})).
  989. // MergeQuery(true).
  990. // ThenScript(`setTimeout(function(){ window.scroll({left: __scrollLeft__, top: __scrollTop__, behavior: "auto"}) }, 50)`).
  991. // Go())+".replace(\"__scrollLeft__\", scrollLeft).replace(\"__scrollTop__\", scrollTop)",
  992. // ),
  993. // ).
  994. Go()
  995. pr.PageTitle = fmt.Sprintf("%s - %s", innerPr.PageTitle, "Page Builder")
  996. pr.Body = VApp(
  997. web.Portal().Name(presets.RightDrawerPortalName),
  998. web.Portal().Name(presets.DialogPortalName),
  999. web.Portal().Name(presets.DeleteConfirmPortalName),
  1000. web.Portal().Name(dialogPortalName),
  1001. h.Script(`
  1002. (function(){
  1003. let scrollLeft = 0;
  1004. let scrollTop = 0;
  1005. function pause(duration) {
  1006. return new Promise(res => setTimeout(res, duration));
  1007. }
  1008. function backoff(retries, fn, delay = 100) {
  1009. fn().catch(err => retries > 1
  1010. ? pause(delay).then(() => backoff(retries - 1, fn, delay * 2))
  1011. : Promise.reject(err));
  1012. }
  1013. function restoreScroll() {
  1014. window.scroll({left: scrollLeft, top: scrollTop, behavior: "auto"});
  1015. if (window.scrollX == scrollLeft && window.scrollY == scrollTop) {
  1016. return Promise.resolve();
  1017. }
  1018. return Promise.reject();
  1019. }
  1020. window.addEventListener('fetchStart', (event) => {
  1021. scrollLeft = window.scrollX;
  1022. scrollTop = window.scrollY;
  1023. });
  1024. window.addEventListener('fetchEnd', (event) => {
  1025. backoff(5, restoreScroll, 100);
  1026. });
  1027. })()
  1028. `),
  1029. vx.VXMessageListener().ListenFunc(fmt.Sprintf(`
  1030. function(e){
  1031. if (!e.data.split) {
  1032. return
  1033. }
  1034. let arr = e.data.split("_");
  1035. if (arr.length != 2) {
  1036. console.log(arr);
  1037. return
  1038. }
  1039. %s
  1040. }`, action)),
  1041. innerPr.Body.(h.HTMLComponent),
  1042. ).Id("vt-app").Attr(web.InitContextVars, `{presetsRightDrawer: false, presetsDialog: false, dialogPortalName: false}`)
  1043. return
  1044. }
  1045. }