editor.go 34 KB

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