editor.go 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153
  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.Drawer).
  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. Go(),
  422. ),
  423. VListItem(
  424. VListItemIcon(VIcon("delete")).Class("pl-0 mr-2"),
  425. VListItemTitle(h.Text("Delete")),
  426. ).Attr("@click", web.Plaid().
  427. URL(web.Var("item.url")).
  428. EventFunc(DeleteContainerConfirmationEvent).
  429. Query(paramContainerID, web.Var("item.param_id")).
  430. Query(paramContainerName, web.Var("item.display_name")).
  431. Go(),
  432. ),
  433. VListItem(
  434. VListItemIcon(VIcon("share")).Class("pl-1 mr-2"),
  435. VListItemTitle(h.Text("Mark As Shared Container")),
  436. ).Attr("@click",
  437. web.Plaid().
  438. URL(web.Var("item.url")).
  439. EventFunc(MarkAsSharedContainerEvent).
  440. Query(paramContainerID, web.Var("item.param_id")).
  441. Go(),
  442. ).Attr("v-if", "!item.shared"),
  443. ).Dense(true),
  444. ).Left(true),
  445. ),
  446. ).Class("pl-0").Attr("@click", fmt.Sprintf(`document.querySelector("iframe").contentWindow.postMessage(%s+"_"+%s,"*");`, web.Var("item.model_name"), web.Var("item.model_id"))),
  447. VDivider().Attr("v-if", "index < locals.items.length "),
  448. ).Attr("v-for", "(item, index) in locals.items", ":key", "item.index"),
  449. h.If(!isReadonly,
  450. VListItem(
  451. VListItemIcon(VIcon("add").Color("primary")).Class("ma-4"),
  452. VListItemTitle(VBtn(msgr.AddContainers).Color("primary").Text(true)),
  453. ).Attr("@click",
  454. web.Plaid().
  455. URL(fmt.Sprintf("%s/editors/%d?version=%s&locale=%s", b.prefix, pageID, pageVersion, locale)).
  456. EventFunc(AddContainerDialogEvent).
  457. Query(paramPageID, pageID).
  458. Query(paramPageVersion, pageVersion).
  459. Query(paramLocale, locale).
  460. Go(),
  461. ),
  462. ),
  463. // ).Class("py-0"),
  464. ),
  465. ).Outlined(true),
  466. ).Class("pa-4 pt-2"),
  467. ).Init(h.JSONString(sorterData)).VSlot("{ locals }")
  468. return
  469. }
  470. func (b *Builder) AddContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  471. pageID := ctx.QueryAsInt(paramPageID)
  472. pageVersion := ctx.R.FormValue(paramPageVersion)
  473. locale := ctx.R.FormValue(paramLocale)
  474. containerName := ctx.R.FormValue(paramContainerName)
  475. sharedContainer := ctx.R.FormValue(paramSharedContainer)
  476. modelID := ctx.QueryAsInt(paramModelID)
  477. var newModelID uint
  478. if sharedContainer == "true" {
  479. err = b.AddSharedContainerToPage(pageID, pageVersion, locale, containerName, uint(modelID))
  480. r.PushState = web.Location(url.Values{})
  481. } else {
  482. newModelID, err = b.AddContainerToPage(pageID, pageVersion, locale, containerName)
  483. r.VarsScript = web.Plaid().
  484. URL(b.ContainerByName(containerName).mb.Info().ListingHref()).
  485. EventFunc(actions.Edit).
  486. Query(presets.ParamOverlay, actions.Drawer).
  487. Query(presets.ParamID, fmt.Sprint(newModelID)).
  488. Go()
  489. }
  490. return
  491. }
  492. func (b *Builder) MoveContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  493. moveResult := ctx.R.FormValue(paramMoveResult)
  494. var result []ContainerSorterItem
  495. err = json.Unmarshal([]byte(moveResult), &result)
  496. if err != nil {
  497. return
  498. }
  499. err = b.db.Transaction(func(tx *gorm.DB) (inerr error) {
  500. for i, r := range result {
  501. if inerr = tx.Model(&Container{}).Where("id = ?", r.ContainerID).Update("display_order", i+1).Error; inerr != nil {
  502. return
  503. }
  504. }
  505. return
  506. })
  507. r.PushState = web.Location(url.Values{})
  508. return
  509. }
  510. func (b *Builder) ToggleContainerVisibility(ctx *web.EventContext) (r web.EventResponse, err error) {
  511. var container Container
  512. paramID := ctx.R.FormValue(paramContainerID)
  513. cs := container.PrimaryColumnValuesBySlug(paramID)
  514. containerID := cs["id"]
  515. locale := cs["locale_code"]
  516. err = b.db.Exec("UPDATE page_builder_containers SET hidden = NOT(coalesce(hidden,FALSE)) WHERE id = ? AND locale_code = ?", containerID, locale).Error
  517. r.PushState = web.Location(url.Values{})
  518. return
  519. }
  520. func (b *Builder) DeleteContainerConfirmation(ctx *web.EventContext) (r web.EventResponse, err error) {
  521. paramID := ctx.R.FormValue(paramContainerID)
  522. containerName := ctx.R.FormValue(paramContainerName)
  523. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  524. Name: presets.DeleteConfirmPortalName,
  525. Body: VDialog(
  526. VCard(
  527. VCardTitle(h.Text(fmt.Sprintf("Are you sure you want to delete %s?", containerName))),
  528. VCardActions(
  529. VSpacer(),
  530. VBtn("Cancel").
  531. Depressed(true).
  532. Class("ml-2").
  533. On("click", "vars.deleteConfirmation = false"),
  534. VBtn("Delete").
  535. Color("primary").
  536. Depressed(true).
  537. Dark(true).
  538. Attr("@click", web.Plaid().
  539. URL(fmt.Sprintf("%s/editors", b.prefix)).
  540. EventFunc(DeleteContainerEvent).
  541. Query(paramContainerID, paramID).
  542. Go()),
  543. ),
  544. ),
  545. ).MaxWidth("600px").
  546. Attr("v-model", "vars.deleteConfirmation").
  547. Attr(web.InitContextVars, `{deleteConfirmation: false}`),
  548. })
  549. r.VarsScript = "setTimeout(function(){ vars.deleteConfirmation = true }, 100)"
  550. return
  551. }
  552. func (b *Builder) DeleteContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  553. var container Container
  554. paramID := ctx.R.FormValue(paramContainerID)
  555. cs := container.PrimaryColumnValuesBySlug(paramID)
  556. containerID := cs["id"]
  557. locale := cs["locale_code"]
  558. err = b.db.Delete(&Container{}, "id = ? AND locale_code = ?", containerID, locale).Error
  559. if err != nil {
  560. return
  561. }
  562. r.PushState = web.Location(url.Values{})
  563. return
  564. }
  565. func (b *Builder) AddContainerToPage(pageID int, pageVersion, locale, containerName string) (modelID uint, err error) {
  566. model := b.ContainerByName(containerName).NewModel()
  567. var dc DemoContainer
  568. b.db.Where("model_name = ? AND locale_code = ?", containerName, locale).First(&dc)
  569. if dc.ID != 0 && dc.ModelID != 0 {
  570. b.db.Where("id = ?", dc.ModelID).First(model)
  571. reflectutils.Set(model, "ID", uint(0))
  572. }
  573. err = b.db.Create(model).Error
  574. if err != nil {
  575. return
  576. }
  577. var maxOrder sql.NullFloat64
  578. err = b.db.Model(&Container{}).Select("MAX(display_order)").Where("page_id = ? and page_version = ? and locale_code = ?", pageID, pageVersion, locale).Scan(&maxOrder).Error
  579. if err != nil {
  580. return
  581. }
  582. modelID = reflectutils.MustGet(model, "ID").(uint)
  583. err = b.db.Create(&Container{
  584. PageID: uint(pageID),
  585. PageVersion: pageVersion,
  586. ModelName: containerName,
  587. DisplayName: containerName,
  588. ModelID: modelID,
  589. DisplayOrder: maxOrder.Float64 + 1,
  590. Locale: l10n.Locale{
  591. LocaleCode: locale,
  592. },
  593. }).Error
  594. if err != nil {
  595. return
  596. }
  597. return
  598. }
  599. func (b *Builder) AddSharedContainerToPage(pageID int, pageVersion, locale, containerName string, modelID uint) (err error) {
  600. var c Container
  601. err = b.db.First(&c, "model_name = ? AND model_id = ? AND shared = true", containerName, modelID).Error
  602. if err != nil {
  603. return
  604. }
  605. var maxOrder sql.NullFloat64
  606. err = b.db.Model(&Container{}).Select("MAX(display_order)").Where("page_id = ? and page_version = ? and locale_code = ?", pageID, pageVersion, locale).Scan(&maxOrder).Error
  607. if err != nil {
  608. return
  609. }
  610. err = b.db.Create(&Container{
  611. PageID: uint(pageID),
  612. PageVersion: pageVersion,
  613. ModelName: containerName,
  614. DisplayName: c.DisplayName,
  615. ModelID: modelID,
  616. Shared: true,
  617. DisplayOrder: maxOrder.Float64 + 1,
  618. Locale: l10n.Locale{
  619. LocaleCode: locale,
  620. },
  621. }).Error
  622. if err != nil {
  623. return
  624. }
  625. return
  626. }
  627. func (b *Builder) copyContainersToNewPageVersion(db *gorm.DB, pageID int, locale, oldPageVersion, newPageVersion string) (err error) {
  628. return b.copyContainersToAnotherPage(db, pageID, oldPageVersion, locale, pageID, newPageVersion, locale)
  629. }
  630. func (b *Builder) copyContainersToAnotherPage(db *gorm.DB, pageID int, pageVersion, locale string, toPageID int, toPageVersion, toPageLocale string) (err error) {
  631. var cons []*Container
  632. err = db.Order("display_order ASC").Find(&cons, "page_id = ? AND page_version = ? AND locale_code = ?", pageID, pageVersion, locale).Error
  633. if err != nil {
  634. return
  635. }
  636. for _, c := range cons {
  637. newModelID := c.ModelID
  638. if !c.Shared {
  639. model := b.ContainerByName(c.ModelName).NewModel()
  640. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  641. return
  642. }
  643. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  644. return
  645. }
  646. if err = db.Create(model).Error; err != nil {
  647. return
  648. }
  649. newModelID = reflectutils.MustGet(model, "ID").(uint)
  650. }
  651. if err = db.Create(&Container{
  652. PageID: uint(toPageID),
  653. PageVersion: toPageVersion,
  654. ModelName: c.ModelName,
  655. DisplayName: c.DisplayName,
  656. ModelID: newModelID,
  657. DisplayOrder: c.DisplayOrder,
  658. Shared: c.Shared,
  659. Locale: l10n.Locale{
  660. LocaleCode: toPageLocale,
  661. },
  662. }).Error; err != nil {
  663. return
  664. }
  665. }
  666. return
  667. }
  668. func (b *Builder) localizeContainersToAnotherPage(db *gorm.DB, pageID int, pageVersion, locale string, toPageID int, toPageVersion, toPageLocale string) (err error) {
  669. var cons []*Container
  670. err = db.Order("display_order ASC").Find(&cons, "page_id = ? AND page_version = ? AND locale_code = ?", pageID, pageVersion, locale).Error
  671. if err != nil {
  672. return
  673. }
  674. for _, c := range cons {
  675. newModelID := c.ModelID
  676. newDisplayName := c.DisplayName
  677. if !c.Shared {
  678. model := b.ContainerByName(c.ModelName).NewModel()
  679. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  680. return
  681. }
  682. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  683. return
  684. }
  685. if err = db.Create(model).Error; err != nil {
  686. return
  687. }
  688. newModelID = reflectutils.MustGet(model, "ID").(uint)
  689. } else {
  690. var count int64
  691. var temp Container
  692. if err = db.Where("model_name = ? AND locale_code = ?", c.ModelName, toPageLocale).First(&temp).Count(&count).Error; err != nil && err != gorm.ErrRecordNotFound {
  693. return
  694. }
  695. if count == 0 {
  696. model := b.ContainerByName(c.ModelName).NewModel()
  697. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  698. return
  699. }
  700. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  701. return
  702. }
  703. if err = db.Create(model).Error; err != nil {
  704. return
  705. }
  706. newModelID = reflectutils.MustGet(model, "ID").(uint)
  707. } else {
  708. newModelID = temp.ModelID
  709. newDisplayName = temp.DisplayName
  710. }
  711. }
  712. var newCon Container
  713. err = db.Order("display_order ASC").Find(&newCon, "id = ? AND locale_code = ?", c.ID, toPageLocale).Error
  714. if err != nil {
  715. return
  716. }
  717. newCon.ID = c.ID
  718. newCon.PageID = uint(toPageID)
  719. newCon.PageVersion = toPageVersion
  720. newCon.ModelName = c.ModelName
  721. newCon.DisplayName = newDisplayName
  722. newCon.ModelID = newModelID
  723. newCon.DisplayOrder = c.DisplayOrder
  724. newCon.Shared = c.Shared
  725. newCon.LocaleCode = toPageLocale
  726. if err = db.Save(&newCon).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.Drawer).
  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. }