editor.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  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. if err = db.Create(&Container{
  713. Model: gorm.Model{ID: c.ID},
  714. PageID: uint(toPageID),
  715. PageVersion: toPageVersion,
  716. ModelName: c.ModelName,
  717. DisplayName: newDisplayName,
  718. ModelID: newModelID,
  719. DisplayOrder: c.DisplayOrder,
  720. Shared: c.Shared,
  721. Locale: l10n.Locale{
  722. LocaleCode: toPageLocale,
  723. },
  724. }).Error; err != nil {
  725. return
  726. }
  727. }
  728. return
  729. }
  730. func (b *Builder) createModelAfterLocalizeDemoContainer(db *gorm.DB, c *DemoContainer) (err error) {
  731. model := b.ContainerByName(c.ModelName).NewModel()
  732. if err = db.First(model, "id = ?", c.ModelID).Error; err != nil {
  733. return
  734. }
  735. if err = reflectutils.Set(model, "ID", uint(0)); err != nil {
  736. return
  737. }
  738. if err = db.Create(model).Error; err != nil {
  739. return
  740. }
  741. c.ModelID = reflectutils.MustGet(model, "ID").(uint)
  742. return
  743. }
  744. func (b *Builder) MarkAsSharedContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  745. var container Container
  746. paramID := ctx.R.FormValue(paramContainerID)
  747. cs := container.PrimaryColumnValuesBySlug(paramID)
  748. containerID := cs["id"]
  749. locale := cs["locale_code"]
  750. err = b.db.Model(&Container{}).Where("id = ? AND locale_code = ?", containerID, locale).Update("shared", true).Error
  751. if err != nil {
  752. return
  753. }
  754. r.PushState = web.Location(url.Values{})
  755. return
  756. }
  757. func (b *Builder) RenameContainer(ctx *web.EventContext) (r web.EventResponse, err error) {
  758. var container Container
  759. paramID := ctx.R.FormValue(paramContainerID)
  760. cs := container.PrimaryColumnValuesBySlug(paramID)
  761. containerID := cs["id"]
  762. locale := cs["locale_code"]
  763. name := ctx.R.FormValue("DisplayName")
  764. var c Container
  765. err = b.db.First(&c, "id = ? AND locale_code = ? ", containerID, locale).Error
  766. if err != nil {
  767. return
  768. }
  769. if c.Shared {
  770. err = b.db.Model(&Container{}).Where("model_name = ? AND model_id = ? AND locale_code = ?", c.ModelName, c.ModelID, locale).Update("display_name", name).Error
  771. if err != nil {
  772. return
  773. }
  774. } else {
  775. err = b.db.Model(&Container{}).Where("id = ? AND locale_code = ?", containerID, locale).Update("display_name", name).Error
  776. if err != nil {
  777. return
  778. }
  779. }
  780. r.PushState = web.Location(url.Values{})
  781. return
  782. }
  783. func (b *Builder) RenameContainerDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  784. paramID := ctx.R.FormValue(paramContainerID)
  785. name := ctx.R.FormValue(paramContainerName)
  786. okAction := web.Plaid().
  787. URL(fmt.Sprintf("%s/editors", b.prefix)).
  788. EventFunc(RenameContainerEvent).Query(paramContainerID, paramID).Go()
  789. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  790. Name: dialogPortalName,
  791. Body: web.Scope(
  792. VDialog(
  793. VCard(
  794. VCardTitle(h.Text("Rename")),
  795. VCardText(
  796. VTextField().FieldName("DisplayName").Value(name),
  797. ),
  798. VCardActions(
  799. VSpacer(),
  800. VBtn("Cancel").
  801. Depressed(true).
  802. Class("ml-2").
  803. On("click", "locals.renameDialog = false"),
  804. VBtn("OK").
  805. Color("primary").
  806. Depressed(true).
  807. Dark(true).
  808. Attr("@click", okAction),
  809. ),
  810. ),
  811. ).MaxWidth("400px").
  812. Attr("v-model", "locals.renameDialog"),
  813. ).Init("{renameDialog:true}").VSlot("{locals}"),
  814. })
  815. return
  816. }
  817. func (b *Builder) AddContainerDialog(ctx *web.EventContext) (r web.EventResponse, err error) {
  818. pageID := ctx.QueryAsInt(paramPageID)
  819. pageVersion := ctx.R.FormValue(paramPageVersion)
  820. locale := ctx.R.FormValue(paramLocale)
  821. // okAction := web.Plaid().EventFunc(RenameContainerEvent).Query(paramContainerID, containerID).Go()
  822. msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
  823. var containers []h.HTMLComponent
  824. for _, builder := range b.containerBuilders {
  825. cover := builder.cover
  826. if cover == "" {
  827. cover = path.Join(b.prefix, b.imagesPrefix, strings.ReplaceAll(builder.name, " ", "")+".png")
  828. }
  829. containers = append(containers,
  830. VCol(
  831. VCard(
  832. VImg().Src(cover).Height(200),
  833. VCardActions(
  834. VCardTitle(h.Text(i18n.T(ctx.R, presets.ModelsI18nModuleKey, builder.name))),
  835. VSpacer(),
  836. VBtn(msgr.Select).
  837. Text(true).
  838. Color("primary").Attr("@click",
  839. "locals.addContainerDialog = false;"+web.Plaid().
  840. URL(fmt.Sprintf("%s/editors/%d?version=%s&locale=%s", b.prefix, pageID, pageVersion, locale)).
  841. EventFunc(AddContainerEvent).
  842. Query(paramPageID, pageID).
  843. Query(paramPageVersion, pageVersion).
  844. Query(paramLocale, locale).
  845. Query(paramContainerName, builder.name).
  846. Go(),
  847. ),
  848. ),
  849. ),
  850. ).Cols(4),
  851. )
  852. }
  853. var cons []*Container
  854. 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
  855. if err != nil {
  856. return
  857. }
  858. var sharedContainers []h.HTMLComponent
  859. for _, sharedC := range cons {
  860. c := b.ContainerByName(sharedC.ModelName)
  861. cover := c.cover
  862. if cover == "" {
  863. cover = path.Join(b.prefix, b.imagesPrefix, strings.ReplaceAll(c.name, " ", "")+".png")
  864. }
  865. sharedContainers = append(sharedContainers,
  866. VCol(
  867. VCard(
  868. VImg().Src(cover).Height(200),
  869. VCardActions(
  870. VCardTitle(h.Text(i18n.T(ctx.R, presets.ModelsI18nModuleKey, sharedC.DisplayName))),
  871. VSpacer(),
  872. VBtn(msgr.Select).
  873. Text(true).
  874. Color("primary").Attr("@click",
  875. "locals.addContainerDialog = false;"+web.Plaid().
  876. URL(fmt.Sprintf("%s/editors/%d?version=%s&locale=%s", b.prefix, pageID, pageVersion, locale)).
  877. EventFunc(AddContainerEvent).
  878. Query(paramPageID, pageID).
  879. Query(paramPageVersion, pageVersion).
  880. Query(paramLocale, locale).
  881. Query(paramContainerName, sharedC.ModelName).
  882. Query(paramModelID, sharedC.ModelID).
  883. Query(paramSharedContainer, "true").
  884. Go(),
  885. ),
  886. ),
  887. ),
  888. ).Cols(4),
  889. )
  890. }
  891. r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
  892. Name: dialogPortalName,
  893. Body: web.Scope(
  894. VDialog(
  895. VTabs(
  896. VTab(h.Text(msgr.New)),
  897. VTabItem(
  898. VSheet(
  899. VContainer(
  900. VRow(
  901. containers...,
  902. ),
  903. ),
  904. ),
  905. ).Attr("style", "overflow-y: scroll; overflow-x: hidden; height: 610px;"),
  906. VTab(h.Text(msgr.Shared)),
  907. VTabItem(
  908. VSheet(
  909. VContainer(
  910. VRow(
  911. sharedContainers...,
  912. ),
  913. ),
  914. ),
  915. ).Attr("style", "overflow-y: scroll; overflow-x: hidden; height: 610px;"),
  916. ),
  917. ).Width("1200px").Attr("v-model", "locals.addContainerDialog"),
  918. ).Init("{addContainerDialog:true}").VSlot("{locals}"),
  919. })
  920. return
  921. }
  922. type editorContainer struct {
  923. builder *ContainerBuilder
  924. container *Container
  925. }
  926. func (b *Builder) getContainerBuilders(cs []*Container) (r []*editorContainer) {
  927. for _, c := range cs {
  928. for _, cb := range b.containerBuilders {
  929. if cb.name == c.ModelName {
  930. r = append(r, &editorContainer{
  931. builder: cb,
  932. container: c,
  933. })
  934. }
  935. }
  936. }
  937. return
  938. }
  939. const (
  940. dialogPortalName = "pagebuilder_DialogPortalName"
  941. )
  942. func (b *Builder) pageEditorLayout(in web.PageFunc, config *presets.LayoutConfig) (out web.PageFunc) {
  943. return func(ctx *web.EventContext) (pr web.PageResponse, err error) {
  944. ctx.Injector.HeadHTML(strings.Replace(`
  945. <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto+Mono">
  946. <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
  947. <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
  948. <link rel="stylesheet" href="{{prefix}}/assets/main.css">
  949. <script src='{{prefix}}/assets/vue.js'></script>
  950. <style>
  951. .page-builder-container {
  952. overflow: hidden;
  953. 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);
  954. }
  955. [v-cloak] {
  956. display: none;
  957. }
  958. </style>
  959. `, "{{prefix}}", b.prefix, -1))
  960. b.ps.InjectExtraAssets(ctx)
  961. if len(os.Getenv("DEV_PRESETS")) > 0 {
  962. ctx.Injector.TailHTML(`
  963. <script src='http://localhost:3080/js/chunk-vendors.js'></script>
  964. <script src='http://localhost:3080/js/app.js'></script>
  965. <script src='http://localhost:3100/js/chunk-vendors.js'></script>
  966. <script src='http://localhost:3100/js/app.js'></script>
  967. `)
  968. } else {
  969. ctx.Injector.TailHTML(strings.Replace(`
  970. <script src='{{prefix}}/assets/main.js'></script>
  971. `, "{{prefix}}", b.prefix, -1))
  972. }
  973. var innerPr web.PageResponse
  974. innerPr, err = in(ctx)
  975. if err != nil {
  976. panic(err)
  977. }
  978. action := web.POST().
  979. EventFunc(actions.Edit).
  980. URL(web.Var("\""+b.prefix+"/\"+arr[0]")).
  981. Query(presets.ParamOverlay, actions.Drawer).
  982. Query(presets.ParamID, web.Var("arr[1]")).
  983. // Query(presets.ParamOverlayAfterUpdateScript,
  984. // web.Var(
  985. // h.JSONString(web.POST().
  986. // PushState(web.Location(url.Values{})).
  987. // MergeQuery(true).
  988. // ThenScript(`setTimeout(function(){ window.scroll({left: __scrollLeft__, top: __scrollTop__, behavior: "auto"}) }, 50)`).
  989. // Go())+".replace(\"__scrollLeft__\", scrollLeft).replace(\"__scrollTop__\", scrollTop)",
  990. // ),
  991. // ).
  992. Go()
  993. pr.PageTitle = fmt.Sprintf("%s - %s", innerPr.PageTitle, "Page Builder")
  994. pr.Body = VApp(
  995. web.Portal().Name(presets.RightDrawerPortalName),
  996. web.Portal().Name(presets.DialogPortalName),
  997. web.Portal().Name(presets.DeleteConfirmPortalName),
  998. web.Portal().Name(dialogPortalName),
  999. h.Script(`
  1000. (function(){
  1001. let scrollLeft = 0;
  1002. let scrollTop = 0;
  1003. function pause(duration) {
  1004. return new Promise(res => setTimeout(res, duration));
  1005. }
  1006. function backoff(retries, fn, delay = 100) {
  1007. fn().catch(err => retries > 1
  1008. ? pause(delay).then(() => backoff(retries - 1, fn, delay * 2))
  1009. : Promise.reject(err));
  1010. }
  1011. function restoreScroll() {
  1012. window.scroll({left: scrollLeft, top: scrollTop, behavior: "auto"});
  1013. if (window.scrollX == scrollLeft && window.scrollY == scrollTop) {
  1014. return Promise.resolve();
  1015. }
  1016. return Promise.reject();
  1017. }
  1018. window.addEventListener('fetchStart', (event) => {
  1019. scrollLeft = window.scrollX;
  1020. scrollTop = window.scrollY;
  1021. });
  1022. window.addEventListener('fetchEnd', (event) => {
  1023. backoff(5, restoreScroll, 100);
  1024. });
  1025. })()
  1026. `),
  1027. vx.VXMessageListener().ListenFunc(fmt.Sprintf(`
  1028. function(e){
  1029. if (!e.data.split) {
  1030. return
  1031. }
  1032. let arr = e.data.split("_");
  1033. if (arr.length != 2) {
  1034. console.log(arr);
  1035. return
  1036. }
  1037. %s
  1038. }`, action)),
  1039. innerPr.Body.(h.HTMLComponent),
  1040. ).Id("vt-app").Attr(web.InitContextVars, `{presetsRightDrawer: false, presetsDialog: false, dialogPortalName: false}`)
  1041. return
  1042. }
  1043. }