editor.go 33 KB

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