editor.go 34 KB

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