diff options
Diffstat (limited to 'addons/website/static/src')
333 files changed, 26476 insertions, 0 deletions
diff --git a/addons/website/static/src/img/SEO-keywords.gif b/addons/website/static/src/img/SEO-keywords.gif Binary files differnew file mode 100644 index 00000000..86b26d12 --- /dev/null +++ b/addons/website/static/src/img/SEO-keywords.gif diff --git a/addons/website/static/src/img/apps_thumbs/website_slide.svg b/addons/website/static/src/img/apps_thumbs/website_slide.svg new file mode 100644 index 00000000..98a4ea0d --- /dev/null +++ b/addons/website/static/src/img/apps_thumbs/website_slide.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#B06161"/><stop offset="45.785%" stop-color="#984E4E"/><stop offset="100%" stop-color="#7C3838"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="#FFF" d="M33.875 44H15a2 2 0 0 1-2-2V22a2 2 0 0 1 2-2h26a2 2 0 0 1 2 2v4.865a7.499 7.499 0 0 0-4 6.635c0 1.946.74 3.718 1.956 5.051a4.099 4.099 0 0 0-.387-.021c-2.049 0-1.256-.22-3.073-2.05-3.187 1.066-6.105 4.876-6.105 5.55.625.317 1.453.973 2.484 1.97zM43 43.132c2.348-1.51 8.09-1.51 10.439 0 1.565 1.007 3.392 3.775 5.48 8.304-1.549 1.77-2.593 2.525-3.132 2.265-1.044-1.51-1.826-2.768-2.348-3.775v5.284c-3.653 1.007-7.306 1.007-10.96 0v-5.284C38.16 45.976 35.334 43.666 34 43c0-.573 0-1 1-2 .667-.667 1.667-1.333 3-2 1.768 3.761 3.434 5.139 5 4.132zm4.74.868l-1.13 4.578 1.695 2.29L50 48.578 48.87 44h-1.13zM22 51l6-7 6 7h-2l-4-5-4 5h-2zm6-31v-2 2zm19.5 21a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13z"/></g></g></svg>
\ No newline at end of file diff --git a/addons/website/static/src/img/backgrounds/building-profile.jpg b/addons/website/static/src/img/backgrounds/building-profile.jpg Binary files differnew file mode 100644 index 00000000..7ab869f0 --- /dev/null +++ b/addons/website/static/src/img/backgrounds/building-profile.jpg diff --git a/addons/website/static/src/img/backgrounds/city.jpg b/addons/website/static/src/img/backgrounds/city.jpg Binary files differnew file mode 100644 index 00000000..31b9a8ce --- /dev/null +++ b/addons/website/static/src/img/backgrounds/city.jpg diff --git a/addons/website/static/src/img/backgrounds/cubes.jpg b/addons/website/static/src/img/backgrounds/cubes.jpg Binary files differnew file mode 100644 index 00000000..b97efc7b --- /dev/null +++ b/addons/website/static/src/img/backgrounds/cubes.jpg diff --git a/addons/website/static/src/img/backgrounds/la.jpg b/addons/website/static/src/img/backgrounds/la.jpg Binary files differnew file mode 100644 index 00000000..38bba3bb --- /dev/null +++ b/addons/website/static/src/img/backgrounds/la.jpg diff --git a/addons/website/static/src/img/backgrounds/panama-sky.jpg b/addons/website/static/src/img/backgrounds/panama-sky.jpg Binary files differnew file mode 100644 index 00000000..72431366 --- /dev/null +++ b/addons/website/static/src/img/backgrounds/panama-sky.jpg diff --git a/addons/website/static/src/img/backgrounds/peak.jpg b/addons/website/static/src/img/backgrounds/peak.jpg Binary files differnew file mode 100644 index 00000000..825d313f --- /dev/null +++ b/addons/website/static/src/img/backgrounds/peak.jpg diff --git a/addons/website/static/src/img/backgrounds/people.jpg b/addons/website/static/src/img/backgrounds/people.jpg Binary files differnew file mode 100644 index 00000000..bd11d9b1 --- /dev/null +++ b/addons/website/static/src/img/backgrounds/people.jpg diff --git a/addons/website/static/src/img/backgrounds/sails.jpg b/addons/website/static/src/img/backgrounds/sails.jpg Binary files differnew file mode 100644 index 00000000..73c80bd8 --- /dev/null +++ b/addons/website/static/src/img/backgrounds/sails.jpg diff --git a/addons/website/static/src/img/backgrounds/type.jpg b/addons/website/static/src/img/backgrounds/type.jpg Binary files differnew file mode 100644 index 00000000..4e86d075 --- /dev/null +++ b/addons/website/static/src/img/backgrounds/type.jpg diff --git a/addons/website/static/src/img/image-search.gif b/addons/website/static/src/img/image-search.gif Binary files differnew file mode 100644 index 00000000..530264a6 --- /dev/null +++ b/addons/website/static/src/img/image-search.gif diff --git a/addons/website/static/src/img/library/bridge.jpg b/addons/website/static/src/img/library/bridge.jpg Binary files differnew file mode 100755 index 00000000..707280e4 --- /dev/null +++ b/addons/website/static/src/img/library/bridge.jpg diff --git a/addons/website/static/src/img/library/business_conference.jpg b/addons/website/static/src/img/library/business_conference.jpg Binary files differnew file mode 100644 index 00000000..41aa570a --- /dev/null +++ b/addons/website/static/src/img/library/business_conference.jpg diff --git a/addons/website/static/src/img/library/clock.jpg b/addons/website/static/src/img/library/clock.jpg Binary files differnew file mode 100755 index 00000000..50aa8bbb --- /dev/null +++ b/addons/website/static/src/img/library/clock.jpg diff --git a/addons/website/static/src/img/library/compass.jpg b/addons/website/static/src/img/library/compass.jpg Binary files differnew file mode 100755 index 00000000..9ccab364 --- /dev/null +++ b/addons/website/static/src/img/library/compass.jpg diff --git a/addons/website/static/src/img/library/deliver.jpg b/addons/website/static/src/img/library/deliver.jpg Binary files differnew file mode 100644 index 00000000..d016b6ba --- /dev/null +++ b/addons/website/static/src/img/library/deliver.jpg diff --git a/addons/website/static/src/img/library/firework.jpg b/addons/website/static/src/img/library/firework.jpg Binary files differnew file mode 100644 index 00000000..b9c178fe --- /dev/null +++ b/addons/website/static/src/img/library/firework.jpg diff --git a/addons/website/static/src/img/library/gift.jpg b/addons/website/static/src/img/library/gift.jpg Binary files differnew file mode 100755 index 00000000..5150b40f --- /dev/null +++ b/addons/website/static/src/img/library/gift.jpg diff --git a/addons/website/static/src/img/library/ice_coffe.jpg b/addons/website/static/src/img/library/ice_coffe.jpg Binary files differnew file mode 100644 index 00000000..6659c3e6 --- /dev/null +++ b/addons/website/static/src/img/library/ice_coffe.jpg diff --git a/addons/website/static/src/img/library/manufacturing.jpg b/addons/website/static/src/img/library/manufacturing.jpg Binary files differnew file mode 100644 index 00000000..a863799e --- /dev/null +++ b/addons/website/static/src/img/library/manufacturing.jpg diff --git a/addons/website/static/src/img/library/marketing.jpg b/addons/website/static/src/img/library/marketing.jpg Binary files differnew file mode 100644 index 00000000..f94c9c68 --- /dev/null +++ b/addons/website/static/src/img/library/marketing.jpg diff --git a/addons/website/static/src/img/library/mobile.jpg b/addons/website/static/src/img/library/mobile.jpg Binary files differnew file mode 100755 index 00000000..b048d414 --- /dev/null +++ b/addons/website/static/src/img/library/mobile.jpg diff --git a/addons/website/static/src/img/library/mobile_device.jpg b/addons/website/static/src/img/library/mobile_device.jpg Binary files differnew file mode 100755 index 00000000..f5691750 --- /dev/null +++ b/addons/website/static/src/img/library/mobile_device.jpg diff --git a/addons/website/static/src/img/library/office.jpg b/addons/website/static/src/img/library/office.jpg Binary files differnew file mode 100644 index 00000000..b46aba6e --- /dev/null +++ b/addons/website/static/src/img/library/office.jpg diff --git a/addons/website/static/src/img/library/rocket.jpg b/addons/website/static/src/img/library/rocket.jpg Binary files differnew file mode 100755 index 00000000..973012aa --- /dev/null +++ b/addons/website/static/src/img/library/rocket.jpg diff --git a/addons/website/static/src/img/library/sell.jpg b/addons/website/static/src/img/library/sell.jpg Binary files differnew file mode 100755 index 00000000..6b66910d --- /dev/null +++ b/addons/website/static/src/img/library/sell.jpg diff --git a/addons/website/static/src/img/library/shop.jpg b/addons/website/static/src/img/library/shop.jpg Binary files differnew file mode 100644 index 00000000..40a59923 --- /dev/null +++ b/addons/website/static/src/img/library/shop.jpg diff --git a/addons/website/static/src/img/library/sign.jpg b/addons/website/static/src/img/library/sign.jpg Binary files differnew file mode 100755 index 00000000..9b8f84c2 --- /dev/null +++ b/addons/website/static/src/img/library/sign.jpg diff --git a/addons/website/static/src/img/library/sweet.jpg b/addons/website/static/src/img/library/sweet.jpg Binary files differnew file mode 100755 index 00000000..1a5f9539 --- /dev/null +++ b/addons/website/static/src/img/library/sweet.jpg diff --git a/addons/website/static/src/img/library/wine.jpg b/addons/website/static/src/img/library/wine.jpg Binary files differnew file mode 100755 index 00000000..a1a7c553 --- /dev/null +++ b/addons/website/static/src/img/library/wine.jpg diff --git a/addons/website/static/src/img/phone.png b/addons/website/static/src/img/phone.png Binary files differnew file mode 100644 index 00000000..0570c4d8 --- /dev/null +++ b/addons/website/static/src/img/phone.png diff --git a/addons/website/static/src/img/snippets_demo/header_image_1_default_image.jpg b/addons/website/static/src/img/snippets_demo/header_image_1_default_image.jpg Binary files differnew file mode 100644 index 00000000..06a5d90d --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/header_image_1_default_image.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_banner.jpg b/addons/website/static/src/img/snippets_demo/s_banner.jpg Binary files differnew file mode 100644 index 00000000..e71228e9 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_banner.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_blockquote_cover.jpg b/addons/website/static/src/img/snippets_demo/s_blockquote_cover.jpg Binary files differnew file mode 100644 index 00000000..c54b0378 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_blockquote_cover.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_carousel_1.jpg b/addons/website/static/src/img/snippets_demo/s_carousel_1.jpg Binary files differnew file mode 100755 index 00000000..18675e7f --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_carousel_1.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_carousel_2.jpg b/addons/website/static/src/img/snippets_demo/s_carousel_2.jpg Binary files differnew file mode 100755 index 00000000..b3426890 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_carousel_2.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_carousel_3.jpg b/addons/website/static/src/img/snippets_demo/s_carousel_3.jpg Binary files differnew file mode 100755 index 00000000..82c7b99c --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_carousel_3.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_cover.jpg b/addons/website/static/src/img/snippets_demo/s_cover.jpg Binary files differnew file mode 100755 index 00000000..6b6934e4 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_cover.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_image_text.jpg b/addons/website/static/src/img/snippets_demo/s_image_text.jpg Binary files differnew file mode 100755 index 00000000..bca5e194 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_image_text.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_masonry_block_1.jpg b/addons/website/static/src/img/snippets_demo/s_masonry_block_1.jpg Binary files differnew file mode 100644 index 00000000..b46aba6e --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_masonry_block_1.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_media_list_1.jpg b/addons/website/static/src/img/snippets_demo/s_media_list_1.jpg Binary files differnew file mode 100755 index 00000000..afd6981a --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_media_list_1.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_media_list_2.jpg b/addons/website/static/src/img/snippets_demo/s_media_list_2.jpg Binary files differnew file mode 100755 index 00000000..721b7d17 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_media_list_2.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_media_list_3.jpg b/addons/website/static/src/img/snippets_demo/s_media_list_3.jpg Binary files differnew file mode 100644 index 00000000..8c9d2b5c --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_media_list_3.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_parallax.jpg b/addons/website/static/src/img/snippets_demo/s_parallax.jpg Binary files differnew file mode 100755 index 00000000..4e8419ae --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_parallax.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_picture.jpg b/addons/website/static/src/img/snippets_demo/s_picture.jpg Binary files differnew file mode 100755 index 00000000..eb58ef41 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_picture.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_popup.jpg b/addons/website/static/src/img/snippets_demo/s_popup.jpg Binary files differnew file mode 100644 index 00000000..3043903e --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_popup.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_product_catalog.jpg b/addons/website/static/src/img/snippets_demo/s_product_catalog.jpg Binary files differnew file mode 100644 index 00000000..3f668b58 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_product_catalog.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_product_list_1.jpg b/addons/website/static/src/img/snippets_demo/s_product_list_1.jpg Binary files differnew file mode 100644 index 00000000..7dd29701 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_product_list_1.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_quotes_carousel_1.jpg b/addons/website/static/src/img/snippets_demo/s_quotes_carousel_1.jpg Binary files differnew file mode 100755 index 00000000..17d82a52 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_quotes_carousel_1.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_quotes_carousel_2.jpg b/addons/website/static/src/img/snippets_demo/s_quotes_carousel_2.jpg Binary files differnew file mode 100755 index 00000000..2b7dc05d --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_quotes_carousel_2.jpg diff --git a/addons/website/static/src/img/snippets_demo/s_references_1.png b/addons/website/static/src/img/snippets_demo/s_references_1.png Binary files differnew file mode 100755 index 00000000..cfced4e5 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_references_1.png diff --git a/addons/website/static/src/img/snippets_demo/s_references_2.png b/addons/website/static/src/img/snippets_demo/s_references_2.png Binary files differnew file mode 100755 index 00000000..75f56c0c --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_references_2.png diff --git a/addons/website/static/src/img/snippets_demo/s_references_3.png b/addons/website/static/src/img/snippets_demo/s_references_3.png Binary files differnew file mode 100755 index 00000000..ee28c154 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_references_3.png diff --git a/addons/website/static/src/img/snippets_demo/s_references_4.png b/addons/website/static/src/img/snippets_demo/s_references_4.png Binary files differnew file mode 100755 index 00000000..a9d30a83 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_references_4.png diff --git a/addons/website/static/src/img/snippets_demo/s_references_5.png b/addons/website/static/src/img/snippets_demo/s_references_5.png Binary files differnew file mode 100755 index 00000000..e4801a37 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_references_5.png diff --git a/addons/website/static/src/img/snippets_demo/s_references_6.png b/addons/website/static/src/img/snippets_demo/s_references_6.png Binary files differnew file mode 100644 index 00000000..896d3d7e --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_references_6.png diff --git a/addons/website/static/src/img/snippets_demo/s_team_member_1.png b/addons/website/static/src/img/snippets_demo/s_team_member_1.png Binary files differnew file mode 100644 index 00000000..a7a887c5 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_team_member_1.png diff --git a/addons/website/static/src/img/snippets_demo/s_team_member_2.png b/addons/website/static/src/img/snippets_demo/s_team_member_2.png Binary files differnew file mode 100644 index 00000000..52316d52 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_team_member_2.png diff --git a/addons/website/static/src/img/snippets_demo/s_team_member_3.png b/addons/website/static/src/img/snippets_demo/s_team_member_3.png Binary files differnew file mode 100644 index 00000000..45bc32e6 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_team_member_3.png diff --git a/addons/website/static/src/img/snippets_demo/s_team_member_4.png b/addons/website/static/src/img/snippets_demo/s_team_member_4.png Binary files differnew file mode 100644 index 00000000..7b1dcb48 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_team_member_4.png diff --git a/addons/website/static/src/img/snippets_demo/s_text_image.jpg b/addons/website/static/src/img/snippets_demo/s_text_image.jpg Binary files differnew file mode 100755 index 00000000..09721c73 --- /dev/null +++ b/addons/website/static/src/img/snippets_demo/s_text_image.jpg diff --git a/addons/website/static/src/img/snippets_options/align_bottom.svg b/addons/website/static/src/img/snippets_options/align_bottom.svg new file mode 100644 index 00000000..d54b039c --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_bottom.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy" transform="translate(-202 -5)"> + <g class="align_bottom" transform="translate(202 5)"> + <rect width="8" height="12" x="12" fill="#B8B8B8" class="o_subdle"/> + <polygon fill="#B8B8B8" points="0 0 9 0 9 1 0 1" class="o_subdle"/> + <rect width="9" height="6" y="6" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/align_bottom_right.svg b/addons/website/static/src/img/snippets_options/align_bottom_right.svg new file mode 100644 index 00000000..9cea3be7 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_bottom_right.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_4" transform="translate(-203 -5)"> + <g class="align_bottom_right" transform="translate(203 5)"> + <rect width="8" height="12" fill="#B8B8B8" class="o_subdle"/> + <rect width="9" height="6" x="11" y="6" fill="#FFF" class="o_graphic"/> + <polygon fill="#B8B8B8" points="11 0 20 0 20 1 11 1" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/align_middle.svg b/addons/website/static/src/img/snippets_options/align_middle.svg new file mode 100644 index 00000000..a2aaa6c3 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_middle.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy" transform="translate(-172 -5)"> + <g class="align_middle" transform="translate(172 5)"> + <rect width="8" height="12" x="12" fill="#B8B8B8" class="o_subdle"/> + <polygon fill="#B8B8B8" points="0 0 9 0 9 1 0 1" class="o_subdle"/> + <rect width="9" height="6" y="3" fill="#FFF" class="o_graphic"/> + <polygon fill="#B8B8B8" points="0 11 9 11 9 12 0 12" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/align_middle_right.svg b/addons/website/static/src/img/snippets_options/align_middle_right.svg new file mode 100644 index 00000000..9ee12c3b --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_middle_right.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_4" transform="translate(-173 -5)"> + <g class="align_middle_right" transform="translate(173 5)"> + <rect width="8" height="12" fill="#B8B8B8" class="o_subdle"/> + <path fill="#B8B8B8" d="M20 11v1h-9v-1h9zM11 0h9v1h-9V0z" class="o_subdle"/> + <rect width="9" height="6" x="11" y="3" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/align_stretch.svg b/addons/website/static/src/img/snippets_options/align_stretch.svg new file mode 100644 index 00000000..026e0486 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_stretch.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy" transform="translate(-234 -5)"> + <g class="align_stretch" transform="translate(234 5)"> + <rect width="8" height="12" x="12" fill="#B8B8B8" class="o_subdle"/> + <rect width="8" height="12" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/align_top.svg b/addons/website/static/src/img/snippets_options/align_top.svg new file mode 100644 index 00000000..5e5be9a1 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_top.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy" transform="translate(-139 -5)"> + <g class="align_top" transform="translate(139 5)"> + <rect width="8" height="12" x="12" fill="#B8B8B8" class="o_subdle"/> + <polygon fill="#B8B8B8" points="0 11 9 11 9 12 0 12" class="o_subdle"/> + <rect width="9" height="6" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/align_top_right.svg b/addons/website/static/src/img/snippets_options/align_top_right.svg new file mode 100644 index 00000000..ed84020e --- /dev/null +++ b/addons/website/static/src/img/snippets_options/align_top_right.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_4" transform="translate(-139 -5)"> + <g class="align_top_right" transform="translate(139 5)"> + <rect width="8" height="12" fill="#B8B8B8" class="o_subdle"/> + <rect width="9" height="1" x="11" y="11" fill="#B8B8B8" class="o_subdle"/> + <rect width="9" height="6" x="11" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/content_width_full.svg b/addons/website/static/src/img/snippets_options/content_width_full.svg new file mode 100644 index 00000000..4620dce4 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/content_width_full.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g fill="#FFF" class="3_buttons_copy_2" transform="translate(-228 -7)"> + <g class="content_width_full" transform="translate(228 7)"> + <path d="M23 0v8H0V0h23zm-5 1v2.5h-3v1h3V7l3-3-3-3zM5 1L2 4l3 3V4.5h3v-1H5V1z" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/content_width_normal.svg b/addons/website/static/src/img/snippets_options/content_width_normal.svg new file mode 100644 index 00000000..2a6b08a8 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/content_width_normal.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_2" transform="translate(-185 -7)"> + <g class="content_width_normal" transform="translate(185 7)"> + <rect width="23" height="8" class="spacer"/> + <rect width="13" height="8" x="5" fill="#FFF" class="o_graphic"/> + <polygon fill="#D8D8D8" points="2 0 2 8 3 8 3 0" class="o_subdle"/> + <polygon fill="#D8D8D8" points="20 0 20 8 21 8 21 0" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/content_width_small.svg b/addons/website/static/src/img/snippets_options/content_width_small.svg new file mode 100644 index 00000000..f52d357b --- /dev/null +++ b/addons/website/static/src/img/snippets_options/content_width_small.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g fill="#FFF" class="3_buttons_copy_2" transform="translate(-141 -7)"> + <g class="content_width_small" transform="translate(141 7)"> + <rect width="7" height="8" x="8" class="o_graphic"/> + <polygon fill-rule="nonzero" points="3 1 6 4 3 7 3 4.5 0 4.5 0 3.5 3 3.5" class="o_graphic"/> + <polygon fill-rule="nonzero" points="20 1 17 4 20 7 20 4.5 23 4.5 23 3.5 20 3.5" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_call_to_action.svg b/addons/website/static/src/img/snippets_options/footer_template_call_to_action.svg new file mode 100644 index 00000000..b7e41925 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_call_to_action.svg @@ -0,0 +1,32 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_call_to_action"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 14)"> + <g class="link"> + <rect width="85" height="8" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 37)"> + <g class="link"> + <rect width="120" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="link" transform="translate(178 13)"> + <rect width="36" height="13" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(195 37)"> + <rect width="19" height="7" class="rectangle" rx="3.5"/> + </g> + <rect width="194" height="1" x="20" y="31" fill="#FFF" class="rectangle" opacity=".324"/> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_centered.svg b/addons/website/static/src/img/snippets_options/footer_template_centered.svg new file mode 100644 index 00000000..c822787b --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_centered.svg @@ -0,0 +1,34 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_centered"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(71 25)"> + <g class="link"> + <rect width="101" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(104 34)"> + <g class="logo_ver"> + <path d="M6.868 11.47V9.605H3.16V3.891H.786v7.58h6.082zm3.945.13c.852 0 1.565-.158 2.136-.473a3.21 3.21 0 0 0 1.31-1.324c.303-.567.454-1.293.454-2.179 0-1.22-.346-2.169-1.038-2.846-.692-.677-1.677-1.016-2.957-1.016-1.247 0-2.22.345-2.92 1.034-.699.69-1.048 1.653-1.048 2.89 0 .886.176 1.624.53 2.213.352.59.813 1.02 1.38 1.292.569.273 1.286.409 2.153.409zm-.079-1.753c-.496 0-.889-.167-1.177-.501-.288-.335-.432-.884-.432-1.65 0-.772.145-1.325.435-1.66.29-.333.674-.5 1.153-.5.5 0 .896.164 1.188.493.291.33.437.85.437 1.564 0 .848-.14 1.436-.42 1.763-.279.327-.674.491-1.184.491zm8.291 1.753c.741 0 1.381-.085 1.921-.256.54-.17 1.162-.492 1.864-.964V7.148h-3.67v1.577h1.589v.714a4.133 4.133 0 0 1-.828.388 2.52 2.52 0 0 1-.76.113c-.56 0-.996-.176-1.309-.527-.312-.352-.469-.938-.469-1.758 0-.772.155-1.336.464-1.693.31-.357.724-.535 1.245-.535.35 0 .637.076.863.227.225.152.385.37.48.657l2.29-.403c-.14-.49-.348-.894-.626-1.213a2.475 2.475 0 0 0-1.049-.705c-.42-.152-1.056-.228-1.905-.228-.881 0-1.582.122-2.103.367a3.343 3.343 0 0 0-1.52 1.393c-.346.612-.519 1.332-.519 2.159 0 .786.158 1.482.474 2.089a3.17 3.17 0 0 0 1.337 1.37c.575.306 1.319.46 2.231.46zm8.244 0c.853 0 1.565-.158 2.136-.473a3.21 3.21 0 0 0 1.31-1.324c.303-.567.454-1.293.454-2.179 0-1.22-.346-2.169-1.038-2.846-.692-.677-1.677-1.016-2.957-1.016-1.247 0-2.22.345-2.92 1.034-.699.69-1.048 1.653-1.048 2.89 0 .886.176 1.624.53 2.213.352.59.813 1.02 1.38 1.292.569.273 1.286.409 2.153.409zm-.079-1.753c-.496 0-.888-.167-1.177-.501-.288-.335-.432-.884-.432-1.65 0-.772.145-1.325.435-1.66.29-.333.674-.5 1.153-.5.5 0 .896.164 1.188.493.292.33.437.85.437 1.564 0 .848-.14 1.436-.419 1.763-.28.327-.675.491-1.185.491z" class="logo"/> + </g> + </g> + <g fill="#FFF" class="link" transform="translate(103 11)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(117 11)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(131 11)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_contact.svg b/addons/website/static/src/img/snippets_options/footer_template_contact.svg new file mode 100644 index 00000000..66e9b4f1 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_contact.svg @@ -0,0 +1,61 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_contact"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(67 15)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(67 22)"> + <g class="link"> + <rect width="15" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(119 22)"> + <g class="link"> + <rect width="18" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(119 15)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 15)"> + <g class="link"> + <rect width="28" height="13" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(172 41)"> + <g class="link"> + <rect width="13" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(145 41)"> + <g class="link"> + <rect width="21" height="4" class="rectangle" rx="1"/> + </g> + </g> + <rect width="194" height="1" x="20" y="32" fill="#FFF" class="rectangle" opacity=".324"/> + <g fill="#FFF" class="group" opacity=".804" transform="translate(191 41)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="link" transform="translate(190 15)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(205 15)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(20 35)"> + <g class="logo_ver"> + <path d="M6.868 11.47V9.605H3.16V3.891H.786v7.58h6.082zm3.945.13c.852 0 1.565-.158 2.136-.473a3.21 3.21 0 0 0 1.31-1.324c.303-.567.454-1.293.454-2.179 0-1.22-.346-2.169-1.038-2.846-.692-.677-1.677-1.016-2.957-1.016-1.247 0-2.22.345-2.92 1.034-.699.69-1.048 1.653-1.048 2.89 0 .886.176 1.624.53 2.213.352.59.813 1.02 1.38 1.292.569.273 1.286.409 2.153.409zm-.079-1.753c-.496 0-.889-.167-1.177-.501-.288-.335-.432-.884-.432-1.65 0-.772.145-1.325.435-1.66.29-.333.674-.5 1.153-.5.5 0 .896.164 1.188.493.291.33.437.85.437 1.564 0 .848-.14 1.436-.42 1.763-.279.327-.674.491-1.184.491zm8.291 1.753c.741 0 1.381-.085 1.921-.256.54-.17 1.162-.492 1.864-.964V7.148h-3.67v1.577h1.589v.714a4.133 4.133 0 0 1-.828.388 2.52 2.52 0 0 1-.76.113c-.56 0-.996-.176-1.309-.527-.312-.352-.469-.938-.469-1.758 0-.772.155-1.336.464-1.693.31-.357.724-.535 1.245-.535.35 0 .637.076.863.227.225.152.385.37.48.657l2.29-.403c-.14-.49-.348-.894-.626-1.213a2.475 2.475 0 0 0-1.049-.705c-.42-.152-1.056-.228-1.905-.228-.881 0-1.582.122-2.103.367a3.343 3.343 0 0 0-1.52 1.393c-.346.612-.519 1.332-.519 2.159 0 .786.158 1.482.474 2.089a3.17 3.17 0 0 0 1.337 1.37c.575.306 1.319.46 2.231.46zm8.244 0c.853 0 1.565-.158 2.136-.473a3.21 3.21 0 0 0 1.31-1.324c.303-.567.454-1.293.454-2.179 0-1.22-.346-2.169-1.038-2.846-.692-.677-1.677-1.016-2.957-1.016-1.247 0-2.22.345-2.92 1.034-.699.69-1.048 1.653-1.048 2.89 0 .886.176 1.624.53 2.213.352.59.813 1.02 1.38 1.292.569.273 1.286.409 2.153.409zm-.079-1.753c-.496 0-.888-.167-1.177-.501-.288-.335-.432-.884-.432-1.65 0-.772.145-1.325.435-1.66.29-.333.674-.5 1.153-.5.5 0 .896.164 1.188.493.292.33.437.85.437 1.564 0 .848-.14 1.436-.419 1.763-.28.327-.675.491-1.185.491z" class="logo"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_default.svg b/addons/website/static/src/img/snippets_options/footer_template_default.svg new file mode 100644 index 00000000..c6a82ccf --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_default.svg @@ -0,0 +1,44 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_default"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 17)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(66 17)"> + <g class="link"> + <rect width="101" height="28" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 25)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(191 17)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(191 25)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="link" transform="translate(191 36)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(205 36)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 33)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 41)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_descriptive.svg b/addons/website/static/src/img/snippets_options/footer_template_descriptive.svg new file mode 100644 index 00000000..0109498f --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_descriptive.svg @@ -0,0 +1,34 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_descriptive"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 17)"> + <g class="link"> + <rect width="101" height="28" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(133 17)"> + <g class="link"> + <rect width="46" height="28" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(191 17)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(191 33)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(191 25)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_headline.svg b/addons/website/static/src/img/snippets_options/footer_template_headline.svg new file mode 100644 index 00000000..84395481 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_headline.svg @@ -0,0 +1,54 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_headline"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(99 16)"> + <g class="link"> + <rect width="67" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 16)"> + <g class="link"> + <rect width="50" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(99 27)"> + <g class="link"> + <rect width="34" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(99 38)"> + <g class="link"> + <rect width="34" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 27)"> + <g class="link"> + <rect width="34" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(20 38)"> + <g class="link"> + <rect width="34" height="6" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="link" transform="translate(177 36)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(191 36)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + <g fill="#FFF" class="link" transform="translate(205 36)"> + <rect width="9" height="9" class="rectangle" rx="4.5"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_links.svg b/addons/website/static/src/img/snippets_options/footer_template_links.svg new file mode 100644 index 00000000..de15305a --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_links.svg @@ -0,0 +1,44 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_links"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group_2" opacity=".804" transform="translate(20 17)"> + <g class="group_copy_3" transform="translate(128)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 8)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 16)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 24)"/> + </g> + <g class="group_copy_2" transform="translate(85)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 8)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 16)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 24)"/> + </g> + <g class="group_copy" transform="translate(43)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 8)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 16)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 24)"/> + </g> + <g class="group"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 8)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 16)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(0 24)"/> + </g> + <g class="link" transform="translate(171)"> + <rect width="23" height="22" class="rectangle" rx="1"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/footer_template_minimalist.svg b/addons/website/static/src/img/snippets_options/footer_template_minimalist.svg new file mode 100644 index 00000000..3ca27f4f --- /dev/null +++ b/addons/website/static/src/img/snippets_options/footer_template_minimalist.svg @@ -0,0 +1,44 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="footer_template_minimalist"> + <g class="rectangle"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-2"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-2"/> + </g> + <g fill="#FFF" class="group_2" transform="translate(19 23)"> + <g class="group" opacity=".804" transform="translate(155 6)"> + <g class="link"> + <rect width="10" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g class="group" opacity=".804" transform="translate(109 6)"> + <g class="link"> + <rect width="10" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g class="group" opacity=".804" transform="translate(38 6)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g class="group" opacity=".804" transform="translate(65 6)"> + <g class="link"> + <rect width="13" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g class="group" opacity=".804" transform="translate(82 6)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <rect width="20" height="9" class="rectangle" rx="4.5" transform="translate(172 3)"/> + <g fill-rule="nonzero" class="group"> + <g class="logo_ver"> + <path d="M6.868 11.47V9.605H3.16V3.891H.786v7.58h6.082zm3.945.13c.852 0 1.565-.158 2.136-.473a3.21 3.21 0 0 0 1.31-1.324c.303-.567.454-1.293.454-2.179 0-1.22-.346-2.169-1.038-2.846-.692-.677-1.677-1.016-2.957-1.016-1.247 0-2.22.345-2.92 1.034-.699.69-1.048 1.653-1.048 2.89 0 .886.176 1.624.53 2.213.352.59.813 1.02 1.38 1.292.569.273 1.286.409 2.153.409zm-.079-1.753c-.496 0-.889-.167-1.177-.501-.288-.335-.432-.884-.432-1.65 0-.772.145-1.325.435-1.66.29-.333.674-.5 1.153-.5.5 0 .896.164 1.188.493.291.33.437.85.437 1.564 0 .848-.14 1.436-.42 1.763-.279.327-.674.491-1.184.491zm8.291 1.753c.741 0 1.381-.085 1.921-.256.54-.17 1.162-.492 1.864-.964V7.148h-3.67v1.577h1.589v.714a4.133 4.133 0 0 1-.828.388 2.52 2.52 0 0 1-.76.113c-.56 0-.996-.176-1.309-.527-.312-.352-.469-.938-.469-1.758 0-.772.155-1.336.464-1.693.31-.357.724-.535 1.245-.535.35 0 .637.076.863.227.225.152.385.37.48.657l2.29-.403c-.14-.49-.348-.894-.626-1.213a2.475 2.475 0 0 0-1.049-.705c-.42-.152-1.056-.228-1.905-.228-.881 0-1.582.122-2.103.367a3.343 3.343 0 0 0-1.52 1.393c-.346.612-.519 1.332-.519 2.159 0 .786.158 1.482.474 2.089a3.17 3.17 0 0 0 1.337 1.37c.575.306 1.319.46 2.231.46zm8.244 0c.853 0 1.565-.158 2.136-.473a3.21 3.21 0 0 0 1.31-1.324c.303-.567.454-1.293.454-2.179 0-1.22-.346-2.169-1.038-2.846-.692-.677-1.677-1.016-2.957-1.016-1.247 0-2.22.345-2.92 1.034-.699.69-1.048 1.653-1.048 2.89 0 .886.176 1.624.53 2.213.352.59.813 1.02 1.38 1.292.569.273 1.286.409 2.153.409zm-.079-1.753c-.496 0-.888-.167-1.177-.501-.288-.335-.432-.884-.432-1.65 0-.772.145-1.325.435-1.66.29-.333.674-.5 1.153-.5.5 0 .896.164 1.188.493.292.33.437.85.437 1.564 0 .848-.14 1.436-.419 1.763-.28.327-.675.491-1.185.491z" class="logo"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_effect_disappears.gif b/addons/website/static/src/img/snippets_options/header_effect_disappears.gif Binary files differnew file mode 100644 index 00000000..75fe9ee4 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_disappears.gif diff --git a/addons/website/static/src/img/snippets_options/header_effect_disappears.png b/addons/website/static/src/img/snippets_options/header_effect_disappears.png Binary files differnew file mode 100644 index 00000000..abf6f008 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_disappears.png diff --git a/addons/website/static/src/img/snippets_options/header_effect_fade_out.gif b/addons/website/static/src/img/snippets_options/header_effect_fade_out.gif Binary files differnew file mode 100644 index 00000000..1619ab31 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_fade_out.gif diff --git a/addons/website/static/src/img/snippets_options/header_effect_fade_out.png b/addons/website/static/src/img/snippets_options/header_effect_fade_out.png Binary files differnew file mode 100644 index 00000000..1485dccf --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_fade_out.png diff --git a/addons/website/static/src/img/snippets_options/header_effect_fixed.gif b/addons/website/static/src/img/snippets_options/header_effect_fixed.gif Binary files differnew file mode 100644 index 00000000..a564094e --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_fixed.gif diff --git a/addons/website/static/src/img/snippets_options/header_effect_fixed.png b/addons/website/static/src/img/snippets_options/header_effect_fixed.png Binary files differnew file mode 100644 index 00000000..4e60b048 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_fixed.png diff --git a/addons/website/static/src/img/snippets_options/header_effect_scroll.gif b/addons/website/static/src/img/snippets_options/header_effect_scroll.gif Binary files differnew file mode 100644 index 00000000..9e6bdd60 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_scroll.gif diff --git a/addons/website/static/src/img/snippets_options/header_effect_scroll.png b/addons/website/static/src/img/snippets_options/header_effect_scroll.png Binary files differnew file mode 100644 index 00000000..73db49c1 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_scroll.png diff --git a/addons/website/static/src/img/snippets_options/header_effect_standard.gif b/addons/website/static/src/img/snippets_options/header_effect_standard.gif Binary files differnew file mode 100644 index 00000000..f34f1925 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_standard.gif diff --git a/addons/website/static/src/img/snippets_options/header_effect_standard.png b/addons/website/static/src/img/snippets_options/header_effect_standard.png Binary files differnew file mode 100644 index 00000000..878fa298 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_effect_standard.png diff --git a/addons/website/static/src/img/snippets_options/header_template_boxed.svg b/addons/website/static/src/img/snippets_options/header_template_boxed.svg new file mode 100644 index 00000000..b1675c3d --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_boxed.svg @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="234px" height="50px" viewBox="0 0 234 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient x1="0%" y1="47.717%" x2="100%" y2="52.283%" id="linearGradient-1"> + <stop stop-color="#00A09D" offset="0%"></stop> + <stop stop-color="#00E2FF" offset="100%"></stop> + </linearGradient> + </defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="header_template_boxed" fill-rule="nonzero"> + <g id="Group"> + <g id="path-23-link" fill="#000000" fill-opacity="0.14"> + <rect id="path-23" x="0" y="0" width="234" height="50"></rect> + </g> + <g id="path-23-link" fill="url(#linearGradient-1)" fill-opacity="0.3"> + <polygon id="path-23" points="0 0 234 0 234 37 224 37 224 50 11 50 10.9978527 37 0 37"></polygon> + </g> + </g> + <g id="Group-3" transform="translate(11, 6)" fill="#FFFFFF"> + <g id="Group" opacity="0.804" transform="translate(181, 3)"> + <g id="Rectangle"> + <rect x="0" y="0" width="17" height="4" rx="1"></rect> + </g> + </g> + <g id="Group-2" transform="translate(0, 3)" opacity="0.804"> + <g id="Rectangle" transform="translate(27, 0)"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + <g id="Rectangle" transform="translate(18, 0)"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + <g id="Rectangle" transform="translate(9, 0)"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Rectangle" transform="translate(204, 0)"> + <rect x="0" y="0" width="9" height="9" rx="4.5"></rect> + </g> + </g> + <g id="Group" opacity="0.4" transform="translate(11, 21)" fill="#FFFFFF"> + <rect id="Rectangle" x="0" y="0" width="213" height="29"></rect> + </g> + <g id="Group" transform="translate(18, 31)" fill="#FFFFFF"> + <path d="M7.676,9 L7.676,6.885 L3.533,6.885 L3.533,0.41 L0.88,0.41 L0.88,9 L7.677,9 L7.676,9 Z M12.085,9.146 C13.038,9.146 13.834,8.968 14.472,8.61 C15.0978687,8.26580349 15.6076733,7.7438192 15.937,7.11 C16.275,6.468 16.444,5.645 16.444,4.64 C16.444,3.258 16.057,2.183 15.284,1.415 C14.511,0.647 13.409,0.264 11.979,0.264 C10.585,0.264 9.497,0.654 8.716,1.436 C7.934,2.216 7.544,3.309 7.544,4.711 C7.544,5.715 7.741,6.551 8.136,7.219 C8.53,7.887 9.045,8.375 9.679,8.684 C10.314,8.992 11.116,9.146 12.085,9.146 L12.085,9.146 Z M11.997,7.16 C11.442,7.16 11.004,6.97 10.681,6.592 C10.359,6.212 10.198,5.59 10.198,4.722 C10.198,3.848 10.36,3.221 10.684,2.842 C11.009,2.462 11.438,2.273 11.974,2.273 C12.532,2.273 12.974,2.46 13.301,2.833 C13.627,3.206 13.79,3.797 13.79,4.605 C13.79,5.566 13.634,6.232 13.321,6.604 C13.009,6.974 12.567,7.16 11.997,7.16 L11.997,7.16 Z M21.263,9.146 C22.091,9.146 22.807,9.05 23.411,8.856 C24.014,8.663 24.709,8.299 25.494,7.764 L25.494,4.102 L21.392,4.102 L21.392,5.889 L23.168,5.889 L23.168,6.697 C22.8767329,6.87778645 22.5661293,7.0253735 22.242,7.137 C21.9670701,7.22381625 21.6803084,7.26733655 21.392,7.266 C20.767,7.266 20.28,7.066 19.93,6.668 C19.58,6.27 19.406,5.605 19.406,4.676 C19.406,3.801 19.579,3.161 19.924,2.756 C20.27,2.353 20.734,2.15 21.316,2.15 C21.706,2.15 22.028,2.236 22.28,2.408 C22.532,2.58 22.71,2.828 22.816,3.152 L25.376,2.695 C25.22,2.141 24.986,1.683 24.676,1.321 C24.3615772,0.956921109 23.9576072,0.681173671 23.504,0.521 C23.034,0.35 22.324,0.264 21.374,0.264 C20.39,0.264 19.607,0.402 19.025,0.68 C18.3059335,1.01379281 17.7108821,1.56649058 17.325,2.259 C16.939,2.952 16.745,3.768 16.745,4.705 C16.745,5.595 16.922,6.385 17.276,7.072 C17.6020876,7.73123825 18.1238794,8.27363635 18.77,8.625 C19.413,8.973 20.244,9.146 21.263,9.146 L21.263,9.146 Z M30.477,9.146 C31.43,9.146 32.226,8.968 32.865,8.61 C33.4904985,8.26564661 33.9999402,7.74367762 34.329,7.11 C34.667,6.468 34.836,5.645 34.836,4.64 C34.836,3.258 34.45,2.183 33.676,1.415 C32.903,0.647 31.801,0.264 30.371,0.264 C28.977,0.264 27.889,0.654 27.108,1.436 C26.327,2.216 25.936,3.309 25.936,4.711 C25.936,5.715 26.133,6.551 26.528,7.219 C26.922,7.887 27.437,8.375 28.072,8.684 C28.706,8.992 29.508,9.146 30.477,9.146 Z M30.389,7.16 C29.834,7.16 29.396,6.97 29.074,6.592 C28.751,6.212 28.59,5.59 28.59,4.722 C28.59,3.848 28.752,3.221 29.077,2.842 C29.401,2.462 29.83,2.273 30.366,2.273 C30.924,2.273 31.366,2.46 31.693,2.833 C32.019,3.206 32.182,3.797 32.182,4.605 C32.182,5.566 32.026,6.232 31.713,6.604 C31.401,6.974 30.959,7.16 30.389,7.16 L30.389,7.16 Z" id="Shape"></path> + </g> + <g id="Group" transform="translate(83, 32)" fill="#FFFFFF"> + <g id="Group-4" transform="translate(0, 3)" opacity="0.804"> + <g id="Group"> + <rect id="Rectangle" x="0" y="0" width="11" height="4" rx="1"></rect> + </g> + <g id="Group" transform="translate(16, 0)"> + <rect id="Rectangle" x="0" y="0" width="23" height="4" rx="1"></rect> + </g> + <g id="Group" transform="translate(71, 0)"> + <rect id="Rectangle" x="0" y="0" width="13" height="4" rx="1"></rect> + </g> + <g id="Group" transform="translate(44, 0)"> + <rect id="Rectangle" x="0" y="0" width="22" height="4" rx="1"></rect> + </g> + </g> + <rect id="Rectangle" x="112" y="0" width="20" height="9" rx="4.5"></rect> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_centered_logo.svg b/addons/website/static/src/img/snippets_options/header_template_centered_logo.svg new file mode 100644 index 00000000..10bf9804 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_centered_logo.svg @@ -0,0 +1,38 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="49.429%" y2="50.571%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-29b" width="234" height="25" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="header_template_centered_logo"> + <g class="bg_1lev"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-29b"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-29b"/> + </g> + <g fill="#FFF" class="group_2" transform="translate(10 9)"> + <g fill-rule="nonzero" class="group" transform="translate(88)"> + <path d="M7.676 13v-2.115H3.533V4.41H.88V13h6.797zm4.409.146c.953 0 1.749-.178 2.387-.536a3.613 3.613 0 0 0 1.465-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.387-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.782.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.543 1.465.635.308 1.437.462 2.406.462zm-.088-1.986c-.555 0-.993-.19-1.316-.568-.322-.38-.483-1.002-.483-1.87 0-.874.162-1.501.486-1.88.325-.38.754-.569 1.29-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556zm9.266 1.986c.828 0 1.544-.096 2.148-.29.603-.193 1.298-.557 2.083-1.092V8.102h-4.102v1.787h1.776v.808a4.59 4.59 0 0 1-.926.44 2.78 2.78 0 0 1-.85.129c-.625 0-1.112-.2-1.462-.598-.35-.398-.524-1.063-.524-1.992 0-.875.173-1.515.518-1.92.346-.403.81-.606 1.392-.606.39 0 .712.086.964.258.252.172.43.42.536.744l2.56-.457c-.156-.554-.39-1.012-.7-1.374a2.765 2.765 0 0 0-1.172-.8c-.47-.171-1.18-.257-2.13-.257-.984 0-1.767.138-2.349.416a3.757 3.757 0 0 0-1.7 1.579c-.386.693-.58 1.509-.58 2.446 0 .89.177 1.68.531 2.367a3.569 3.569 0 0 0 1.494 1.553c.643.348 1.474.521 2.493.521zm9.214 0c.953 0 1.749-.178 2.388-.536a3.613 3.613 0 0 0 1.464-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.386-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.781.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.544 1.465.634.308 1.436.462 2.405.462zm-.088-1.986c-.555 0-.993-.19-1.315-.568-.323-.38-.484-1.002-.484-1.87 0-.874.162-1.501.487-1.88.324-.38.753-.569 1.289-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556z" class="logo"/> + </g> + <g class="group" opacity=".804" transform="translate(0 7)"> + <g class="link"> + <rect width="15" height="4" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(18)"> + <rect width="15" height="4" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(37)"> + <rect width="15" height="4" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(173)"> + <rect width="15" height="4" class="rectangle" rx="1"/> + </g> + </g> + <rect width="20" height="9" class="rectangle" rx="4.5" transform="translate(194 4)"/> + </g> + <g fill="#FFF" class="page_content" opacity=".442" transform="translate(0 30)"> + <rect width="234" height="25" class="rectangle_v1"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_contact.svg b/addons/website/static/src/img/snippets_options/header_template_contact.svg new file mode 100644 index 00000000..3af85000 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_contact.svg @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="234px" height="50px" viewBox="0 0 234 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient x1="0%" y1="47.717%" x2="100%" y2="52.283%" id="linearGradient-1"> + <stop stop-color="#00A09D" offset="0%"></stop> + <stop stop-color="#00E2FF" offset="100%"></stop> + </linearGradient> + </defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="header_template_contact" fill-rule="nonzero"> + <g id="Group"> + <g id="path-27-link" fill="#000000" fill-opacity="0.14"> + <rect id="path-27" x="0" y="0" width="234" height="50"></rect> + </g> + <g id="path-27-link" fill="url(#linearGradient-1)" fill-opacity="0.3"> + <rect id="path-27" x="0" y="0" width="234" height="50"></rect> + </g> + </g> + <g id="Group" transform="translate(10.000000, 31.000000)" fill="#FFFFFF"> + <path d="M7.676,9 L7.676,6.885 L3.533,6.885 L3.533,0.41 L0.88,0.41 L0.88,9 L7.677,9 L7.676,9 Z M12.085,9.146 C13.038,9.146 13.834,8.968 14.472,8.61 C15.0978687,8.26580349 15.6076733,7.7438192 15.937,7.11 C16.275,6.468 16.444,5.645 16.444,4.64 C16.444,3.258 16.057,2.183 15.284,1.415 C14.511,0.647 13.409,0.264 11.979,0.264 C10.585,0.264 9.497,0.654 8.716,1.436 C7.934,2.216 7.544,3.309 7.544,4.711 C7.544,5.715 7.741,6.551 8.136,7.219 C8.53,7.887 9.045,8.375 9.679,8.684 C10.314,8.992 11.116,9.146 12.085,9.146 L12.085,9.146 Z M11.997,7.16 C11.442,7.16 11.004,6.97 10.681,6.592 C10.359,6.212 10.198,5.59 10.198,4.722 C10.198,3.848 10.36,3.221 10.684,2.842 C11.009,2.462 11.438,2.273 11.974,2.273 C12.532,2.273 12.974,2.46 13.301,2.833 C13.627,3.206 13.79,3.797 13.79,4.605 C13.79,5.566 13.634,6.232 13.321,6.604 C13.009,6.974 12.567,7.16 11.997,7.16 L11.997,7.16 Z M21.263,9.146 C22.091,9.146 22.807,9.05 23.411,8.856 C24.014,8.663 24.709,8.299 25.494,7.764 L25.494,4.102 L21.392,4.102 L21.392,5.889 L23.168,5.889 L23.168,6.697 C22.8767329,6.87778645 22.5661293,7.0253735 22.242,7.137 C21.9670701,7.22381625 21.6803084,7.26733655 21.392,7.266 C20.767,7.266 20.28,7.066 19.93,6.668 C19.58,6.27 19.406,5.605 19.406,4.676 C19.406,3.801 19.579,3.161 19.924,2.756 C20.27,2.353 20.734,2.15 21.316,2.15 C21.706,2.15 22.028,2.236 22.28,2.408 C22.532,2.58 22.71,2.828 22.816,3.152 L25.376,2.695 C25.22,2.141 24.986,1.683 24.676,1.321 C24.3615772,0.956921109 23.9576072,0.681173671 23.504,0.521 C23.034,0.35 22.324,0.264 21.374,0.264 C20.39,0.264 19.607,0.402 19.025,0.68 C18.3059335,1.01379281 17.7108821,1.56649058 17.325,2.259 C16.939,2.952 16.745,3.768 16.745,4.705 C16.745,5.595 16.922,6.385 17.276,7.072 C17.6020876,7.73123825 18.1238794,8.27363635 18.77,8.625 C19.413,8.973 20.244,9.146 21.263,9.146 L21.263,9.146 Z M30.477,9.146 C31.43,9.146 32.226,8.968 32.865,8.61 C33.4904985,8.26564661 33.9999402,7.74367762 34.329,7.11 C34.667,6.468 34.836,5.645 34.836,4.64 C34.836,3.258 34.45,2.183 33.676,1.415 C32.903,0.647 31.801,0.264 30.371,0.264 C28.977,0.264 27.889,0.654 27.108,1.436 C26.327,2.216 25.936,3.309 25.936,4.711 C25.936,5.715 26.133,6.551 26.528,7.219 C26.922,7.887 27.437,8.375 28.072,8.684 C28.706,8.992 29.508,9.146 30.477,9.146 Z M30.389,7.16 C29.834,7.16 29.396,6.97 29.074,6.592 C28.751,6.212 28.59,5.59 28.59,4.722 C28.59,3.848 28.752,3.221 29.077,2.842 C29.401,2.462 29.83,2.273 30.366,2.273 C30.924,2.273 31.366,2.46 31.693,2.833 C32.019,3.206 32.182,3.797 32.182,4.605 C32.182,5.566 32.026,6.232 31.713,6.604 C31.401,6.974 30.959,7.16 30.389,7.16 L30.389,7.16 Z" id="Shape"></path> + </g> + <g id="Group" opacity="0.804" transform="translate(56.000000, 34.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="13" height="4" rx="1"></rect> + </g> + <g transform="translate(18.000000, 0.000000)" id="Rectangle"> + <rect x="0" y="0" width="23" height="4" rx="1"></rect> + </g> + <g transform="translate(46.000000, 0.000000)" id="Rectangle"> + <rect x="0" y="0" width="16" height="4" rx="1"></rect> + </g> + <g transform="translate(67.000000, 0.000000)" id="Rectangle"> + <rect x="0" y="0" width="23" height="4" rx="1"></rect> + </g> + </g> + <rect id="Rectangle" fill="#FFFFFF" x="204" y="31" width="20" height="9" rx="4.5"></rect> + <g id="Group" opacity="0.804" transform="translate(21.000000, 12.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="17" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(43.000000, 12.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="17" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(218.000000, 12.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(209.000000, 12.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(200.000000, 12.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(191.000000, 12.000000)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group-Copy" opacity="0.804" transform="translate(10.000000, 12.000000)" fill="#FFFFFF"> + <g id="Group"> + <rect id="Rectangle" x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_default.svg b/addons/website/static/src/img/snippets_options/header_template_default.svg new file mode 100644 index 00000000..0f9ce829 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_default.svg @@ -0,0 +1,28 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="49.429%" y2="50.571%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-22" width="234" height="25" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="header_template_default"> + <g class="bg_def"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-22"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-22"/> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(10 9)"> + <path d="M7.676 13v-2.115H3.533V4.41H.88V13h6.797zm4.409.146c.953 0 1.749-.178 2.387-.536a3.613 3.613 0 0 0 1.465-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.387-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.782.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.543 1.465.635.308 1.437.462 2.406.462zm-.088-1.986c-.555 0-.993-.19-1.316-.568-.322-.38-.483-1.002-.483-1.87 0-.874.162-1.501.486-1.88.325-.38.754-.569 1.29-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556zm9.266 1.986c.828 0 1.544-.096 2.148-.29.603-.193 1.298-.557 2.083-1.092V8.102h-4.102v1.787h1.776v.808a4.59 4.59 0 0 1-.926.44 2.78 2.78 0 0 1-.85.129c-.625 0-1.112-.2-1.462-.598-.35-.398-.524-1.063-.524-1.992 0-.875.173-1.515.518-1.92.346-.403.81-.606 1.392-.606.39 0 .712.086.964.258.252.172.43.42.536.744l2.56-.457c-.156-.554-.39-1.012-.7-1.374a2.765 2.765 0 0 0-1.172-.8c-.47-.171-1.18-.257-2.13-.257-.984 0-1.767.138-2.349.416a3.757 3.757 0 0 0-1.7 1.579c-.386.693-.58 1.509-.58 2.446 0 .89.177 1.68.531 2.367a3.569 3.569 0 0 0 1.494 1.553c.643.348 1.474.521 2.493.521zm9.214 0c.953 0 1.749-.178 2.388-.536a3.613 3.613 0 0 0 1.464-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.386-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.781.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.544 1.465.634.308 1.436.462 2.405.462zm-.088-1.986c-.555 0-.993-.19-1.315-.568-.323-.38-.484-1.002-.484-1.87 0-.874.162-1.501.487-1.88.324-.38.753-.569 1.289-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556z" class="logo"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(71 16)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(28)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(56)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(84)"/> + </g> + <rect width="20" height="9" fill="#FFF" class="rectangle" rx="4.5" transform="translate(204 13)"/> + <g fill="#FFF" class="page_content_default" opacity=".442" transform="translate(0 30)"> + <rect width="234" height="25" class="rectangle_default"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_hamburger.svg b/addons/website/static/src/img/snippets_options/header_template_hamburger.svg new file mode 100644 index 00000000..b6f82dac --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_hamburger.svg @@ -0,0 +1,26 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="49.429%" y2="50.571%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-24" width="234" height="25" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="header_template_hamburger"> + <g class="bg_small_ham"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-24"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-24"/> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(10 9)"> + <path d="M7.676 13v-2.115H3.533V4.41H.88V13h6.797zm4.409.146c.953 0 1.749-.178 2.387-.536a3.613 3.613 0 0 0 1.465-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.387-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.782.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.543 1.465.635.308 1.437.462 2.406.462zm-.088-1.986c-.555 0-.993-.19-1.316-.568-.322-.38-.483-1.002-.483-1.87 0-.874.162-1.501.486-1.88.325-.38.754-.569 1.29-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556zm9.266 1.986c.828 0 1.544-.096 2.148-.29.603-.193 1.298-.557 2.083-1.092V8.102h-4.102v1.787h1.776v.808a4.59 4.59 0 0 1-.926.44 2.78 2.78 0 0 1-.85.129c-.625 0-1.112-.2-1.462-.598-.35-.398-.524-1.063-.524-1.992 0-.875.173-1.515.518-1.92.346-.403.81-.606 1.392-.606.39 0 .712.086.964.258.252.172.43.42.536.744l2.56-.457c-.156-.554-.39-1.012-.7-1.374a2.765 2.765 0 0 0-1.172-.8c-.47-.171-1.18-.257-2.13-.257-.984 0-1.767.138-2.349.416a3.757 3.757 0 0 0-1.7 1.579c-.386.693-.58 1.509-.58 2.446 0 .89.177 1.68.531 2.367a3.569 3.569 0 0 0 1.494 1.553c.643.348 1.474.521 2.493.521zm9.214 0c.953 0 1.749-.178 2.388-.536a3.613 3.613 0 0 0 1.464-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.386-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.781.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.544 1.465.634.308 1.436.462 2.405.462zm-.088-1.986c-.555 0-.993-.19-1.315-.568-.323-.38-.484-1.002-.484-1.87 0-.874.162-1.501.487-1.88.324-.38.753-.569 1.289-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556z" class="logo"/> + </g> + <g fill="#FFF" class="group" transform="translate(208 12)"> + <rect width="16" height="2" class="rectangle_ham" rx="1"/> + <rect width="16" height="2" y="4" class="rectangle_ham" rx="1"/> + <rect width="16" height="2" y="8" class="rectangle_ham" rx="1"/> + </g> + <g fill="#FFF" class="page_content" opacity=".442" transform="translate(0 30)"> + <rect width="234" height="25" class="rectangle_ham"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_hamburger_full.svg b/addons/website/static/src/img/snippets_options/header_template_hamburger_full.svg new file mode 100644 index 00000000..d859e88d --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_hamburger_full.svg @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="234px" height="50px" viewBox="0 0 234 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient x1="0%" y1="47.716%" x2="100%" y2="52.284%" id="linearGradient-1"> + <stop stop-color="#00A09D" offset="0%"></stop> + <stop stop-color="#00E2FF" offset="100%"></stop> + </linearGradient> + </defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="header_template_hamburger" fill-rule="nonzero"> + <g id="Group"> + <g id="path-24-link" fill="#000000" fill-opacity="0.14"> + <rect id="path-24" x="0" y="0" width="234" height="25"></rect> + </g> + <g id="path-24-link" fill="url(#linearGradient-1)" fill-opacity="0.3"> + <rect id="path-24" x="0" y="0" width="234" height="50"></rect> + </g> + </g> + <g id="Group" transform="translate(10, 8)" fill="#FFFFFF"> + <path d="M7.676,9 L7.676,6.885 L3.533,6.885 L3.533,0.41 L0.88,0.41 L0.88,9 L7.677,9 L7.676,9 Z M12.085,9.146 C13.038,9.146 13.834,8.968 14.472,8.61 C15.0978687,8.26580349 15.6076733,7.7438192 15.937,7.11 C16.275,6.468 16.444,5.645 16.444,4.64 C16.444,3.258 16.057,2.183 15.284,1.415 C14.511,0.647 13.409,0.264 11.979,0.264 C10.585,0.264 9.497,0.654 8.716,1.436 C7.934,2.216 7.544,3.309 7.544,4.711 C7.544,5.715 7.741,6.551 8.136,7.219 C8.53,7.887 9.045,8.375 9.679,8.684 C10.314,8.992 11.116,9.146 12.085,9.146 L12.085,9.146 Z M11.997,7.16 C11.442,7.16 11.004,6.97 10.681,6.592 C10.359,6.212 10.198,5.59 10.198,4.722 C10.198,3.848 10.36,3.221 10.684,2.842 C11.009,2.462 11.438,2.273 11.974,2.273 C12.532,2.273 12.974,2.46 13.301,2.833 C13.627,3.206 13.79,3.797 13.79,4.605 C13.79,5.566 13.634,6.232 13.321,6.604 C13.009,6.974 12.567,7.16 11.997,7.16 L11.997,7.16 Z M21.263,9.146 C22.091,9.146 22.807,9.05 23.411,8.856 C24.014,8.663 24.709,8.299 25.494,7.764 L25.494,4.102 L21.392,4.102 L21.392,5.889 L23.168,5.889 L23.168,6.697 C22.8767329,6.87778645 22.5661293,7.0253735 22.242,7.137 C21.9670701,7.22381625 21.6803084,7.26733655 21.392,7.266 C20.767,7.266 20.28,7.066 19.93,6.668 C19.58,6.27 19.406,5.605 19.406,4.676 C19.406,3.801 19.579,3.161 19.924,2.756 C20.27,2.353 20.734,2.15 21.316,2.15 C21.706,2.15 22.028,2.236 22.28,2.408 C22.532,2.58 22.71,2.828 22.816,3.152 L25.376,2.695 C25.22,2.141 24.986,1.683 24.676,1.321 C24.3615772,0.956921109 23.9576072,0.681173671 23.504,0.521 C23.034,0.35 22.324,0.264 21.374,0.264 C20.39,0.264 19.607,0.402 19.025,0.68 C18.3059335,1.01379281 17.7108821,1.56649058 17.325,2.259 C16.939,2.952 16.745,3.768 16.745,4.705 C16.745,5.595 16.922,6.385 17.276,7.072 C17.6020876,7.73123825 18.1238794,8.27363635 18.77,8.625 C19.413,8.973 20.244,9.146 21.263,9.146 L21.263,9.146 Z M30.477,9.146 C31.43,9.146 32.226,8.968 32.865,8.61 C33.4904985,8.26564661 33.9999402,7.74367762 34.329,7.11 C34.667,6.468 34.836,5.645 34.836,4.64 C34.836,3.258 34.45,2.183 33.676,1.415 C32.903,0.647 31.801,0.264 30.371,0.264 C28.977,0.264 27.889,0.654 27.108,1.436 C26.327,2.216 25.936,3.309 25.936,4.711 C25.936,5.715 26.133,6.551 26.528,7.219 C26.922,7.887 27.437,8.375 28.072,8.684 C28.706,8.992 29.508,9.146 30.477,9.146 Z M30.389,7.16 C29.834,7.16 29.396,6.97 29.074,6.592 C28.751,6.212 28.59,5.59 28.59,4.722 C28.59,3.848 28.752,3.221 29.077,2.842 C29.401,2.462 29.83,2.273 30.366,2.273 C30.924,2.273 31.366,2.46 31.693,2.833 C32.019,3.206 32.182,3.797 32.182,4.605 C32.182,5.566 32.026,6.232 31.713,6.604 C31.401,6.974 30.959,7.16 30.389,7.16 L30.389,7.16 Z" id="Shape"></path> + </g> + <g id="Group" transform="translate(208, 7)" fill="#FFFFFF"> + <rect id="Rectangle" x="0" y="0" width="16" height="2" rx="1"></rect> + <rect id="Rectangle" x="0" y="4" width="16" height="2" rx="1"></rect> + <rect id="Rectangle" x="0" y="8" width="16" height="2" rx="1"></rect> + </g> + <g id="Group" opacity="0.804" transform="translate(73, 39)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="13" height="4" rx="1"></rect> + </g> + <g id="Rectangle" transform="translate(18, 0)"> + <rect x="0" y="0" width="23" height="4" rx="1"></rect> + </g> + <g id="Rectangle" transform="translate(46, 0)"> + <rect x="0" y="0" width="16" height="4" rx="1"></rect> + </g> + <g id="Rectangle" transform="translate(67, 0)"> + <rect x="0" y="0" width="23" height="4" rx="1"></rect> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_image.svg b/addons/website/static/src/img/snippets_options/header_template_image.svg new file mode 100644 index 00000000..6d7b6e66 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_image.svg @@ -0,0 +1,53 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="49.429%" y2="50.571%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-28" width="234" height="25" x="0" y="30"/> + <rect id="path-38" width="234" height="25" x="0" y="0"/> + <linearGradient id="linearGradient-5" x1="72.875%" x2="40.332%" y1="47.355%" y2="38.066%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-6" x1="88.517%" x2="50%" y1="43.259%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <linearGradient id="linearGradient-7" x1="88.517%" x2="50%" y1="43.604%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="header_template_image"> + <g class="bg_top"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-28"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-28"/> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(10 33)"> + <path d="M7.676 13v-2.115H3.533V4.41H.88V13h6.797zm4.409.146c.953 0 1.749-.178 2.387-.536a3.613 3.613 0 0 0 1.465-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.387-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.782.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.543 1.465.635.308 1.437.462 2.406.462zm-.088-1.986c-.555 0-.993-.19-1.316-.568-.322-.38-.483-1.002-.483-1.87 0-.874.162-1.501.486-1.88.325-.38.754-.569 1.29-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556zm9.266 1.986c.828 0 1.544-.096 2.148-.29.603-.193 1.298-.557 2.083-1.092V8.102h-4.102v1.787h1.776v.808a4.59 4.59 0 0 1-.926.44 2.78 2.78 0 0 1-.85.129c-.625 0-1.112-.2-1.462-.598-.35-.398-.524-1.063-.524-1.992 0-.875.173-1.515.518-1.92.346-.403.81-.606 1.392-.606.39 0 .712.086.964.258.252.172.43.42.536.744l2.56-.457c-.156-.554-.39-1.012-.7-1.374a2.765 2.765 0 0 0-1.172-.8c-.47-.171-1.18-.257-2.13-.257-.984 0-1.767.138-2.349.416a3.757 3.757 0 0 0-1.7 1.579c-.386.693-.58 1.509-.58 2.446 0 .89.177 1.68.531 2.367a3.569 3.569 0 0 0 1.494 1.553c.643.348 1.474.521 2.493.521zm9.214 0c.953 0 1.749-.178 2.388-.536a3.613 3.613 0 0 0 1.464-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.386-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.781.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.544 1.465.634.308 1.436.462 2.405.462zm-.088-1.986c-.555 0-.993-.19-1.315-.568-.323-.38-.484-1.002-.484-1.87 0-.874.162-1.501.487-1.88.324-.38.753-.569 1.289-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556z" class="logo"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(71 40)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(28)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(56)"/> + <rect width="23" height="4" class="rectangle" rx="1" transform="translate(84)"/> + </g> + <rect width="20" height="9" fill="#FFF" class="rectangle" rx="4.5" transform="translate(204 37)"/> + <g class="image_1_border_long" transform="translate(0 5)"> + <rect width="234" height="25" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask"> + <mask id="mask-4" fill="#fff"> + <use xlink:href="#path-38"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-38"/> + <ellipse cx="51.5" cy="29.5" fill="url(#linearGradient-5)" class="oval" mask="url(#mask-4)" rx="23.5" ry="12.5"/> + <ellipse cx="3.5" cy="27" fill="url(#linearGradient-6)" class="oval" mask="url(#mask-4)" rx="37.5" ry="18"/> + <ellipse cx="221.5" cy="25" fill="url(#linearGradient-7)" class="oval" mask="url(#mask-4)" rx="38.5" ry="18"/> + <circle cx="129.5" cy="9.5" r="5.5" fill="#F3EC60" class="oval" mask="url(#mask-4)"/> + <ellipse cx="174.5" cy="27.5" fill="url(#linearGradient-5)" class="oval" mask="url(#mask-4)" rx="23.5" ry="12.5"/> + </g> + </g> + <rect width="234" height="1" y="30" fill="#FFF" class="rectangle_topbar"/> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_magazine.svg b/addons/website/static/src/img/snippets_options/header_template_magazine.svg new file mode 100644 index 00000000..0951eed8 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_magazine.svg @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="234px" height="50px" viewBox="0 0 234 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient x1="0%" y1="47.717%" x2="100%" y2="52.283%" id="linearGradient-1"> + <stop stop-color="#00A09D" offset="0%"></stop> + <stop stop-color="#00E2FF" offset="100%"></stop> + </linearGradient> + </defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="header_template_contact" fill-rule="nonzero"> + <g id="Group"> + <g id="path-27-link" fill="#000000" fill-opacity="0.14"> + <rect id="path-27" x="0" y="0" width="234" height="50"></rect> + </g> + <g id="path-27-link" fill="url(#linearGradient-1)" fill-opacity="0.3"> + <rect id="path-27" x="0" y="0" width="234" height="50"></rect> + </g> + </g> + <g id="Group" transform="translate(10, 31)" fill="#FFFFFF"> + <path d="M7.676,9 L7.676,6.885 L3.533,6.885 L3.533,0.41 L0.88,0.41 L0.88,9 L7.677,9 L7.676,9 Z M12.085,9.146 C13.038,9.146 13.834,8.968 14.472,8.61 C15.0978687,8.26580349 15.6076733,7.7438192 15.937,7.11 C16.275,6.468 16.444,5.645 16.444,4.64 C16.444,3.258 16.057,2.183 15.284,1.415 C14.511,0.647 13.409,0.264 11.979,0.264 C10.585,0.264 9.497,0.654 8.716,1.436 C7.934,2.216 7.544,3.309 7.544,4.711 C7.544,5.715 7.741,6.551 8.136,7.219 C8.53,7.887 9.045,8.375 9.679,8.684 C10.314,8.992 11.116,9.146 12.085,9.146 L12.085,9.146 Z M11.997,7.16 C11.442,7.16 11.004,6.97 10.681,6.592 C10.359,6.212 10.198,5.59 10.198,4.722 C10.198,3.848 10.36,3.221 10.684,2.842 C11.009,2.462 11.438,2.273 11.974,2.273 C12.532,2.273 12.974,2.46 13.301,2.833 C13.627,3.206 13.79,3.797 13.79,4.605 C13.79,5.566 13.634,6.232 13.321,6.604 C13.009,6.974 12.567,7.16 11.997,7.16 L11.997,7.16 Z M21.263,9.146 C22.091,9.146 22.807,9.05 23.411,8.856 C24.014,8.663 24.709,8.299 25.494,7.764 L25.494,4.102 L21.392,4.102 L21.392,5.889 L23.168,5.889 L23.168,6.697 C22.8767329,6.87778645 22.5661293,7.0253735 22.242,7.137 C21.9670701,7.22381625 21.6803084,7.26733655 21.392,7.266 C20.767,7.266 20.28,7.066 19.93,6.668 C19.58,6.27 19.406,5.605 19.406,4.676 C19.406,3.801 19.579,3.161 19.924,2.756 C20.27,2.353 20.734,2.15 21.316,2.15 C21.706,2.15 22.028,2.236 22.28,2.408 C22.532,2.58 22.71,2.828 22.816,3.152 L25.376,2.695 C25.22,2.141 24.986,1.683 24.676,1.321 C24.3615772,0.956921109 23.9576072,0.681173671 23.504,0.521 C23.034,0.35 22.324,0.264 21.374,0.264 C20.39,0.264 19.607,0.402 19.025,0.68 C18.3059335,1.01379281 17.7108821,1.56649058 17.325,2.259 C16.939,2.952 16.745,3.768 16.745,4.705 C16.745,5.595 16.922,6.385 17.276,7.072 C17.6020876,7.73123825 18.1238794,8.27363635 18.77,8.625 C19.413,8.973 20.244,9.146 21.263,9.146 L21.263,9.146 Z M30.477,9.146 C31.43,9.146 32.226,8.968 32.865,8.61 C33.4904985,8.26564661 33.9999402,7.74367762 34.329,7.11 C34.667,6.468 34.836,5.645 34.836,4.64 C34.836,3.258 34.45,2.183 33.676,1.415 C32.903,0.647 31.801,0.264 30.371,0.264 C28.977,0.264 27.889,0.654 27.108,1.436 C26.327,2.216 25.936,3.309 25.936,4.711 C25.936,5.715 26.133,6.551 26.528,7.219 C26.922,7.887 27.437,8.375 28.072,8.684 C28.706,8.992 29.508,9.146 30.477,9.146 Z M30.389,7.16 C29.834,7.16 29.396,6.97 29.074,6.592 C28.751,6.212 28.59,5.59 28.59,4.722 C28.59,3.848 28.752,3.221 29.077,2.842 C29.401,2.462 29.83,2.273 30.366,2.273 C30.924,2.273 31.366,2.46 31.693,2.833 C32.019,3.206 32.182,3.797 32.182,4.605 C32.182,5.566 32.026,6.232 31.713,6.604 C31.401,6.974 30.959,7.16 30.389,7.16 L30.389,7.16 Z" id="Shape"></path> + </g> + <g id="Group" opacity="0.804" transform="translate(10, 8)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="17" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(218, 8)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(209, 8)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(200, 8)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + <g id="Group" opacity="0.804" transform="translate(191, 8)" fill="#FFFFFF"> + <g id="Rectangle"> + <rect x="0" y="0" width="6" height="4" rx="1"></rect> + </g> + </g> + </g> + <rect id="Rectangle" fill="#FFFFFF" fill-rule="nonzero" opacity="0.375" x="10" y="20" width="214" height="1"></rect> + <g id="Group" transform="translate(208, 31)" fill="#FFFFFF" fill-rule="nonzero"> + <rect id="Rectangle" x="0" y="0" width="16" height="2" rx="1"></rect> + <rect id="Rectangle" x="0" y="4" width="16" height="2" rx="1"></rect> + <rect id="Rectangle" x="0" y="8" width="16" height="2" rx="1"></rect> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_minimalist.svg b/addons/website/static/src/img/snippets_options/header_template_minimalist.svg new file mode 100644 index 00000000..a765b16c --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_minimalist.svg @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="234px" height="50px" viewBox="0 0 234 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient x1="0%" y1="49.429%" x2="100%" y2="50.571%" id="linearGradient-1"> + <stop stop-color="#00A09D" offset="0%"></stop> + <stop stop-color="#00E2FF" offset="100%"></stop> + </linearGradient> + </defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="header_template_minimalist" fill-rule="nonzero"> + <g id="Group"> + <g id="path-25-link" fill="#000000" fill-opacity="0.14"> + <rect id="path-25" x="0" y="0" width="234" height="25"></rect> + </g> + <g id="path-25-link" fill="url(#linearGradient-1)" fill-opacity="0.3"> + <rect id="path-25" x="0" y="0" width="234" height="25"></rect> + </g> + </g> + <g id="Group" transform="translate(10, 8)" fill="#FFFFFF"> + <g id="Shape"> + <path d="M7.676,9 L7.676,6.885 L3.533,6.885 L3.533,0.41 L0.88,0.41 L0.88,9 L7.677,9 L7.676,9 Z M12.085,9.146 C13.038,9.146 13.834,8.968 14.472,8.61 C15.0978687,8.26580349 15.6076733,7.7438192 15.937,7.11 C16.275,6.468 16.444,5.645 16.444,4.64 C16.444,3.258 16.057,2.183 15.284,1.415 C14.511,0.647 13.409,0.264 11.979,0.264 C10.585,0.264 9.497,0.654 8.716,1.436 C7.934,2.216 7.544,3.309 7.544,4.711 C7.544,5.715 7.741,6.551 8.136,7.219 C8.53,7.887 9.045,8.375 9.679,8.684 C10.314,8.992 11.116,9.146 12.085,9.146 L12.085,9.146 Z M11.997,7.16 C11.442,7.16 11.004,6.97 10.681,6.592 C10.359,6.212 10.198,5.59 10.198,4.722 C10.198,3.848 10.36,3.221 10.684,2.842 C11.009,2.462 11.438,2.273 11.974,2.273 C12.532,2.273 12.974,2.46 13.301,2.833 C13.627,3.206 13.79,3.797 13.79,4.605 C13.79,5.566 13.634,6.232 13.321,6.604 C13.009,6.974 12.567,7.16 11.997,7.16 L11.997,7.16 Z M21.263,9.146 C22.091,9.146 22.807,9.05 23.411,8.856 C24.014,8.663 24.709,8.299 25.494,7.764 L25.494,4.102 L21.392,4.102 L21.392,5.889 L23.168,5.889 L23.168,6.697 C22.8767329,6.87778645 22.5661293,7.0253735 22.242,7.137 C21.9670701,7.22381625 21.6803084,7.26733655 21.392,7.266 C20.767,7.266 20.28,7.066 19.93,6.668 C19.58,6.27 19.406,5.605 19.406,4.676 C19.406,3.801 19.579,3.161 19.924,2.756 C20.27,2.353 20.734,2.15 21.316,2.15 C21.706,2.15 22.028,2.236 22.28,2.408 C22.532,2.58 22.71,2.828 22.816,3.152 L25.376,2.695 C25.22,2.141 24.986,1.683 24.676,1.321 C24.3615772,0.956921109 23.9576072,0.681173671 23.504,0.521 C23.034,0.35 22.324,0.264 21.374,0.264 C20.39,0.264 19.607,0.402 19.025,0.68 C18.3059335,1.01379281 17.7108821,1.56649058 17.325,2.259 C16.939,2.952 16.745,3.768 16.745,4.705 C16.745,5.595 16.922,6.385 17.276,7.072 C17.6020876,7.73123825 18.1238794,8.27363635 18.77,8.625 C19.413,8.973 20.244,9.146 21.263,9.146 L21.263,9.146 Z M30.477,9.146 C31.43,9.146 32.226,8.968 32.865,8.61 C33.4904985,8.26564661 33.9999402,7.74367762 34.329,7.11 C34.667,6.468 34.836,5.645 34.836,4.64 C34.836,3.258 34.45,2.183 33.676,1.415 C32.903,0.647 31.801,0.264 30.371,0.264 C28.977,0.264 27.889,0.654 27.108,1.436 C26.327,2.216 25.936,3.309 25.936,4.711 C25.936,5.715 26.133,6.551 26.528,7.219 C26.922,7.887 27.437,8.375 28.072,8.684 C28.706,8.992 29.508,9.146 30.477,9.146 Z M30.389,7.16 C29.834,7.16 29.396,6.97 29.074,6.592 C28.751,6.212 28.59,5.59 28.59,4.722 C28.59,3.848 28.752,3.221 29.077,2.842 C29.401,2.462 29.83,2.273 30.366,2.273 C30.924,2.273 31.366,2.46 31.693,2.833 C32.019,3.206 32.182,3.797 32.182,4.605 C32.182,5.566 32.026,6.232 31.713,6.604 C31.401,6.974 30.959,7.16 30.389,7.16 L30.389,7.16 Z"></path> + </g> + <g opacity="0.804" transform="translate(45, 3)" id="Rectangle"> + <g> + <rect x="0" y="0" width="12" height="4" rx="1"></rect> + </g> + <g transform="translate(17, 0)"> + <rect x="0" y="0" width="12" height="4" rx="1"></rect> + </g> + <g transform="translate(34, 0)"> + <rect x="0" y="0" width="18" height="4" rx="1"></rect> + </g> + </g> + </g> + <g id="Group" opacity="0.442" transform="translate(0, 25)" fill="#FFFFFF"> + <rect id="Rectangle" x="0" y="0" width="234" height="25"></rect> + </g> + </g> + <g id="Group" transform="translate(216, 8)" fill="#FFFFFF" fill-rule="nonzero"> + <rect id="Rectangle" x="0" y="0" width="9" height="9" rx="4.5"></rect> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_sidebar.svg b/addons/website/static/src/img/snippets_options/header_template_sidebar.svg new file mode 100644 index 00000000..f1b7a202 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_sidebar.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="18.506%" y2="81.494%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-26" width="63" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="header_template_sidebar"> + <g class="side_bg_side"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-26"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-26"/> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(10 9)"> + <path d="M7.676 13v-2.115H3.533V4.41H.88V13h6.797zm4.409.146c.953 0 1.749-.178 2.387-.536a3.613 3.613 0 0 0 1.465-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.387-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.782.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.543 1.465.635.308 1.437.462 2.406.462zm-.088-1.986c-.555 0-.993-.19-1.316-.568-.322-.38-.483-1.002-.483-1.87 0-.874.162-1.501.486-1.88.325-.38.754-.569 1.29-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556zm9.266 1.986c.828 0 1.544-.096 2.148-.29.603-.193 1.298-.557 2.083-1.092V8.102h-4.102v1.787h1.776v.808a4.59 4.59 0 0 1-.926.44 2.78 2.78 0 0 1-.85.129c-.625 0-1.112-.2-1.462-.598-.35-.398-.524-1.063-.524-1.992 0-.875.173-1.515.518-1.92.346-.403.81-.606 1.392-.606.39 0 .712.086.964.258.252.172.43.42.536.744l2.56-.457c-.156-.554-.39-1.012-.7-1.374a2.765 2.765 0 0 0-1.172-.8c-.47-.171-1.18-.257-2.13-.257-.984 0-1.767.138-2.349.416a3.757 3.757 0 0 0-1.7 1.579c-.386.693-.58 1.509-.58 2.446 0 .89.177 1.68.531 2.367a3.569 3.569 0 0 0 1.494 1.553c.643.348 1.474.521 2.493.521zm9.214 0c.953 0 1.749-.178 2.388-.536a3.613 3.613 0 0 0 1.464-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.386-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.781.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.544 1.465.634.308 1.436.462 2.405.462zm-.088-1.986c-.555 0-.993-.19-1.315-.568-.323-.38-.484-1.002-.484-1.87 0-.874.162-1.501.487-1.88.324-.38.753-.569 1.289-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556z" class="logo"/> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(10 34)"> + <g class="link"> + <rect width="23" height="3" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(0 6)"> + <rect width="37" height="3" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(0 12)"> + <rect width="30" height="3" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(0 18)"> + <rect width="26" height="3" class="rectangle" rx="1"/> + </g> + </g> + <rect width="1" height="51" x="62" y="5" fill="#FFF" class="bar_side" opacity=".5"/> + <g fill="#FFF" class="page_content" opacity=".4" transform="translate(63 5)"> + <rect width="171" height="50" class="rectangle_side"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_slogan.svg b/addons/website/static/src/img/snippets_options/header_template_slogan.svg new file mode 100644 index 00000000..5bb81d4a --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_slogan.svg @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="234px" height="50px" viewBox="0 0 234 50" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <linearGradient x1="0%" y1="47.717%" x2="100%" y2="52.283%" id="linearGradient-1"> + <stop stop-color="#00A09D" offset="0%"></stop> + <stop stop-color="#00E2FF" offset="100%"></stop> + </linearGradient> + </defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> + <g id="header_template_slogan" fill-rule="nonzero"> + <g id="Group"> + <g id="path-21-link" fill="#000000" fill-opacity="0.14"> + <rect id="path-21" x="0" y="0" width="234" height="50"></rect> + </g> + <g id="path-21-link" fill="url(#linearGradient-1)" fill-opacity="0.3"> + <rect id="path-21" x="0" y="0" width="234" height="50"></rect> + </g> + </g> + <text id="LOGO" fill="#FFFFFF" font-family="Arial-Black, Arial Black" font-size="12" font-weight="700"> + <tspan x="10" y="18">LOGO</tspan> + </text> + <rect id="Rectangle" fill="#FFFFFF" opacity="0.375" x="10" y="26" width="214" height="1"></rect> + <g id="Group" opacity="0.804" transform="translate(10, 36)" fill="#FFFFFF"> + <rect id="Rectangle" x="0" y="0" width="23" height="4" rx="1"></rect> + <g transform="translate(28, 0)" id="Rectangle"> + <rect x="0" y="0" width="9" height="4" rx="1"></rect> + </g> + <g id="Group-Copy" transform="translate(204, 0)"> + <rect id="Rectangle" x="0" y="0" width="9" height="4" rx="1"></rect> + </g> + <g transform="translate(42, 0)" id="Rectangle"> + <rect x="0" y="0" width="23" height="4" rx="1"></rect> + </g> + <g transform="translate(70, 0)" id="Rectangle"> + <rect x="0" y="0" width="16" height="4" rx="1"></rect> + </g> + </g> + <rect id="Rectangle" fill="#FFFFFF" x="204" y="11" width="20" height="9" rx="4.5"></rect> + <path d="M59.227,17 L59.699,15.7 L61.989,15.7 L62.489,17 L63.746,17 L61.453,11.273 L60.23,11.273 L58,17 L59.227,17 Z M61.617,14.734 L60.055,14.734 L60.828,12.609 L61.618,14.734 L61.617,14.734 Z M68.117,17 L68.82,14.332 L69.535,17 L70.59,17 L71.922,12.852 L70.84,12.852 L70.047,15.57 L69.355,12.852 L68.297,12.852 L67.582,15.57 L66.805,12.852 L65.738,12.852 L67.051,17 L68.117,17 Z M74.391,17.094 C75.005,17.094 75.515,16.887 75.92,16.474 C76.325,16.062 76.527,15.542 76.527,14.914 C76.527,14.292 76.327,13.777 75.926,13.369 C75.525,12.962 75.01,12.758 74.383,12.758 C73.977,12.758 73.609,12.848 73.279,13.028 C72.9516526,13.2048745 72.6856193,13.4768351 72.516,13.808 C72.336,14.15 72.246,14.503 72.246,14.868 C72.246,15.344 72.336,15.748 72.516,16.08 C72.696,16.412 72.958,16.664 73.303,16.836 C73.648,17.008 74.01,17.094 74.391,17.094 Z M74.387,16.199 C74.1090953,16.2031961 73.8438798,16.082877 73.664,15.871 C73.469,15.652 73.371,15.337 73.371,14.926 C73.371,14.514 73.469,14.199 73.664,13.98 C73.8438798,13.768123 74.1090953,13.6478039 74.387,13.652 C74.673,13.652 74.913,13.762 75.107,13.98 C75.301,14.2 75.398,14.512 75.398,14.918 C75.398,15.335 75.301,15.652 75.108,15.871 C74.9290044,16.0829098 74.6643566,16.2033044 74.387,16.199 L74.387,16.199 Z M78.477,17 L78.477,15.121 C78.477,14.658 78.505,14.34 78.561,14.168 C78.6147422,13.9990169 78.7239772,13.8531353 78.871,13.754 C79.0213295,13.6501005 79.2002756,13.5955778 79.383,13.598 C79.5179113,13.5939992 79.6507222,13.6320949 79.763,13.707 C79.8706854,13.7819375 79.9513416,13.889596 79.993,14.014 C80.039,14.145 80.063,14.434 80.063,14.883 L80.063,17 L81.16,17 L81.16,14.422 C81.16,14.102 81.14,13.855 81.1,13.684 C81.0618077,13.5168351 80.9885219,13.3596967 80.885,13.223 C80.7645517,13.0744203 80.6078461,12.9593881 80.43,12.889 C80.2202434,12.7994433 79.9940529,12.7548183 79.766,12.758 C79.216,12.758 78.76,12.992 78.398,13.461 L78.398,12.851 L77.378,12.851 L77.378,17 L78.477,17 L78.477,17 Z M83.762,17.094 C83.996,17.094 84.229,17.036 84.459,16.92 C84.689,16.804 84.889,16.628 85.059,16.39 L85.059,17 L86.079,17 L86.079,11.273 L84.98,11.273 L84.98,13.336 C84.642,12.951 84.24,12.758 83.777,12.758 C83.272,12.758 82.854,12.941 82.523,13.307 C82.193,13.673 82.027,14.207 82.027,14.91 C82.027,15.598 82.197,16.133 82.537,16.518 C82.877,16.902 83.285,17.094 83.762,17.094 L83.762,17.094 Z M84.074,16.227 C83.757,16.227 83.509,16.083 83.332,15.797 C83.21,15.599 83.148,15.279 83.148,14.836 C83.148,14.424 83.236,14.116 83.412,13.908 C83.5706199,13.7094553 83.8118901,13.5950917 84.066,13.598 C84.335,13.598 84.555,13.702 84.726,13.912 C84.898,14.122 84.984,14.465 84.984,14.942 C84.984,15.368 84.896,15.689 84.719,15.904 C84.542,16.119 84.327,16.227 84.074,16.227 L84.074,16.227 Z M88.852,17.094 C89.307,17.094 89.687,16.989 89.99,16.779 C90.294,16.569 90.516,16.264 90.656,15.863 L89.562,15.68 C89.502,15.888 89.414,16.04 89.297,16.133 C89.1730736,16.2288263 89.0195626,16.278346 88.863,16.273 C88.6227147,16.2785791 88.3918095,16.179724 88.23,16.002 C88.061,15.821 87.973,15.568 87.965,15.242 L90.715,15.242 C90.73,14.401 90.56,13.777 90.203,13.369 C89.846,12.962 89.359,12.758 88.743,12.758 C88.193,12.758 87.738,12.952 87.379,13.342 C87.019,13.732 86.839,14.27 86.839,14.957 C86.839,15.533 86.977,16.009 87.25,16.387 C87.596,16.858 88.13,17.094 88.852,17.094 L88.852,17.094 Z M89.625,14.57 L87.985,14.57 C87.982,14.27 88.059,14.034 88.215,13.86 C88.3640317,13.6884692 88.5818395,13.5923991 88.809,13.598 C89.033,13.598 89.223,13.68 89.379,13.846 C89.535,14.011 89.617,14.253 89.625,14.57 L89.625,14.57 Z M92.66,17 L92.66,15.719 C92.66,15.013 92.69,14.549 92.752,14.329 C92.813,14.107 92.897,13.954 93.004,13.869 C93.1154442,13.7829203 93.253238,13.7380489 93.394,13.742 C93.554,13.742 93.725,13.802 93.91,13.922 L94.25,12.965 C94.0325359,12.8310194 93.7824202,12.7594095 93.527,12.758 C93.3594604,12.7555439 93.1948061,12.8017444 93.053,12.891 C92.913,12.979 92.757,13.163 92.582,13.441 L92.582,12.851 L91.562,12.851 L91.562,17 L92.66,17 L92.66,17 Z M95.95,17 L95.95,13.715 L96.77,13.715 L96.77,12.852 L95.95,12.852 L95.95,12.559 C95.95,12.348 95.984,12.205 96.053,12.131 C96.122,12.057 96.236,12.02 96.395,12.02 C96.556,12.02 96.724,12.04 96.898,12.082 L97.047,11.316 C96.747,11.223 96.441,11.176 96.129,11.176 C95.824,11.176 95.572,11.23 95.373,11.338 C95.1922962,11.4269548 95.0475753,11.5752054 94.963,11.758 C94.889,11.93 94.852,12.19 94.852,12.539 L94.852,12.852 L94.242,12.852 L94.242,13.715 L94.852,13.715 L94.852,17 L95.949,17 L95.95,17 Z M98.742,17.094 C99.016,17.094 99.275,17.03 99.522,16.902 C99.768,16.775 99.966,16.6 100.117,16.379 L100.117,17 L101.137,17 L101.137,12.852 L100.039,12.852 L100.039,14.602 C100.039,15.195 100.012,15.568 99.957,15.721 C99.8987642,15.8779176 99.7918979,16.0121136 99.652,16.104 C99.5041371,16.2065674 99.3279381,16.2604059 99.148,16.258 C99.0038794,16.263829 98.8616554,16.2235439 98.742,16.143 C98.6356588,16.0662727 98.5581801,15.9560418 98.522,15.83 C98.482,15.699 98.461,15.341 98.461,14.758 L98.461,12.852 L97.363,12.852 L97.363,15.477 C97.363,15.867 97.413,16.173 97.512,16.395 C97.611,16.615 97.771,16.788 97.992,16.91 C98.214,17.033 98.464,17.094 98.742,17.094 L98.742,17.094 Z M103.372,17 L103.372,11.273 L102.273,11.273 L102.273,17 L103.371,17 L103.372,17 Z M108.32,17.094 C108.943,17.094 109.413,16.957 109.73,16.684 C110.048,16.41 110.207,16.084 110.207,15.704 C110.207,15.354 110.092,15.082 109.863,14.887 C109.632,14.694 109.223,14.531 108.639,14.398 C108.054,14.266 107.712,14.163 107.613,14.09 C107.54,14.035 107.503,13.969 107.503,13.89 C107.503283,13.7990563 107.551064,13.7148707 107.629,13.668 C107.754,13.588 107.961,13.547 108.25,13.547 C108.48,13.547 108.656,13.59 108.78,13.676 C108.905427,13.7651477 108.994535,13.8965022 109.031,14.046 L110.066,13.856 C109.962,13.493 109.772,13.22 109.496,13.036 C109.22,12.85 108.798,12.758 108.23,12.758 C107.634,12.758 107.194,12.88 106.91,13.125 C106.637097,13.3461006 106.480196,13.6797921 106.484,14.031 C106.484,14.43 106.648,14.741 106.977,14.965 C107.214,15.126 107.775,15.305 108.66,15.5 C108.85,15.544 108.973,15.592 109.027,15.645 C109.079519,15.7008657 109.107592,15.7753679 109.105,15.852 C109.10828,15.9674429 109.054401,16.0770728 108.961,16.145 C108.818,16.249 108.604,16.301 108.321,16.301 C108.063,16.301 107.862,16.245 107.719,16.135 C107.568493,16.01225 107.467303,15.8393392 107.434,15.648 L106.332,15.816 C106.434,16.21 106.649,16.521 106.979,16.75 C107.308,16.98 107.755,17.094 108.32,17.094 L108.32,17.094 Z M112.266,17 L112.266,11.273 L111.168,11.273 L111.168,17 L112.266,17 Z M115.281,17.094 C115.896,17.094 116.406,16.887 116.811,16.474 C117.215,16.062 117.418,15.542 117.418,14.914 C117.418,14.292 117.218,13.777 116.816,13.369 C116.416,12.962 115.901,12.758 115.273,12.758 C114.867,12.758 114.499,12.848 114.17,13.028 C113.84,13.207 113.586,13.468 113.406,13.808 C113.226,14.15 113.136,14.503 113.136,14.868 C113.136,15.344 113.226,15.748 113.406,16.08 C113.586,16.412 113.848,16.664 114.193,16.836 C114.538,17.008 114.901,17.094 115.281,17.094 L115.281,17.094 Z M115.277,16.199 C114.999443,16.2028959 114.734656,16.0826047 114.555,15.871 C114.359,15.652 114.262,15.337 114.262,14.926 C114.262,14.514 114.359,14.199 114.555,13.98 C114.734656,13.7683953 114.999443,13.6481041 115.277,13.652 C115.564,13.652 115.804,13.762 115.998,13.98 C116.192,14.2 116.289,14.512 116.289,14.918 C116.289,15.335 116.192,15.652 115.998,15.871 C115.819228,16.0826367 115.555008,16.2030032 115.278,16.199 L115.277,16.199 Z M120.109,18.684 C120.482,18.684 120.789,18.644 121.033,18.566 C121.249541,18.5032594 121.449155,18.3925148 121.617,18.242 C121.775667,18.0829942 121.892479,17.887163 121.957,17.672 C122.037,17.43 122.078,17.064 122.078,16.574 L122.078,12.852 L121.051,12.852 L121.051,13.434 C120.717,12.984 120.294,12.758 119.781,12.758 C119.304336,12.7487635 118.847803,12.9499597 118.533,13.308 C118.199,13.676 118.031,14.207 118.031,14.902 C118.031,15.457 118.159,15.921 118.414,16.293 C118.74,16.764 119.181,17 119.738,17 C120.238,17 120.652,16.776 120.98,16.328 L120.98,16.934 C120.98,17.178 120.964,17.346 120.93,17.437 C120.89068,17.5521335 120.813016,17.6502729 120.71,17.715 C120.568,17.801 120.353,17.844 120.066,17.844 C119.842,17.844 119.678,17.804 119.574,17.727 C119.499,17.672 119.451,17.572 119.43,17.426 L118.176,17.273 C118.173584,17.3172942 118.17225,17.3616407 118.172,17.406 C118.172,17.776 118.319,18.081 118.613,18.322 C118.908,18.563 119.406,18.684 120.109,18.684 L120.109,18.684 Z M120.043,16.114012 C119.796651,16.1153551 119.563735,16.0018549 119.413,15.807 C119.242,15.602 119.156,15.28 119.156,14.84 C119.156,14.42 119.242,14.109 119.412,13.904 C119.569108,13.7062377 119.809475,13.5930805 120.062,13.598 C120.333,13.598 120.557,13.702 120.734,13.91 C120.911,14.118 121,14.437 121,14.867 C121,15.279 120.908,15.589 120.723,15.799 C120.555117,16.0006405 120.305364,16.115968 120.043,16.113 L120.043,16.114012 Z M124.253,17.094 C124.496,17.094 124.724,17.048 124.938,16.957 C125.161938,16.8586276 125.36571,16.7196147 125.539,16.547 C125.547,16.567 125.559,16.611 125.579,16.676 C125.619,16.819 125.655,16.927 125.684,17 L126.77,17 C126.680857,16.823391 126.614333,16.6362491 126.572,16.443 C126.537,16.27 126.52,16.001 126.52,15.637 L126.531,14.355 C126.531,13.879 126.482,13.551 126.385,13.373 C126.287,13.195 126.118,13.048 125.879,12.932 C125.639,12.816 125.275,12.758 124.785,12.758 C124.246,12.758 123.84,12.854 123.566,13.047 C123.293,13.24 123.1,13.537 122.988,13.937 L123.984,14.117 C124.052,13.924 124.141,13.79 124.25,13.713 C124.36,13.636 124.512,13.598 124.707,13.598 C124.997,13.598 125.193,13.643 125.297,13.732 C125.401,13.822 125.453,13.972 125.453,14.184 L125.453,14.293 C125.255,14.376 124.9,14.466 124.387,14.563 C124.007,14.635 123.715,14.721 123.514,14.818 C123.318979,14.9093823 123.155171,15.0561485 123.043,15.24 C122.929485,15.4291845 122.871283,15.6464042 122.875,15.867 C122.875,16.221 122.998,16.514 123.245,16.747 C123.49,16.977 123.827,17.094 124.254,17.094 L124.253,17.094 Z M124.582,16.32 C124.419594,16.3254873 124.261907,16.2648662 124.145,16.152 C124.03438,16.0493311 123.971971,15.9049193 123.972987,15.754 C123.972987,15.598 124.044,15.469 124.187,15.367 C124.281,15.305 124.48,15.241 124.785,15.176 C125.09,15.111 125.313,15.055 125.453,15.008 L125.453,15.227 C125.453,15.49 125.439,15.668 125.41,15.762 C125.365576,15.9088245 125.273359,16.0365915 125.148,16.125 C124.985279,16.2492997 124.786752,16.3176969 124.582,16.32 L124.582,16.32 Z M128.703,17 L128.703,15.121 C128.703,14.658 128.731,14.34 128.787,14.168 C128.840742,13.9990169 128.949977,13.8531353 129.097,13.754 C129.247329,13.6501005 129.426276,13.5955778 129.609,13.598 C129.74425,13.5937898 129.877442,13.6318946 129.99,13.707 C130.097313,13.7820943 130.177605,13.8897353 130.219,14.014 C130.266,14.145 130.289,14.434 130.289,14.883 L130.289,17 L131.387,17 L131.387,14.422 C131.387,14.102 131.367,13.855 131.327,13.684 C131.288507,13.5167542 131.214878,13.3596098 131.111,13.223 C130.990552,13.0744203 130.833846,12.9593881 130.656,12.889 C130.446243,12.7994433 130.220053,12.7548183 129.992,12.758 C129.442,12.758 128.987,12.992 128.625,13.461 L128.625,12.851 L127.605,12.851 L127.605,17 L128.703,17 L128.703,17 Z" id="Shape" fill="#FFFFFF"></path> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/header_template_vertical.svg b/addons/website/static/src/img/snippets_options/header_template_vertical.svg new file mode 100644 index 00000000..b38e7169 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/header_template_vertical.svg @@ -0,0 +1,35 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="234" height="60" viewBox="0 0 234 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="47.717%" y2="52.283%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-29" width="234" height="50" x="0" y="5"/> + </defs> + <g fill="none" fill-rule="evenodd" class="header_template_vertical"> + <g class="bg_ver"> + <use fill="#000" fill-opacity=".14" xlink:href="#path-29"/> + <use fill="url(#linearGradient-1)" fill-opacity=".3" xlink:href="#path-29"/> + </g> + <g fill="#FFF" fill-rule="nonzero" class="group" transform="translate(98 10)"> + <path d="M7.676 13v-2.115H3.533V4.41H.88V13h6.797zm4.409.146c.953 0 1.749-.178 2.387-.536a3.613 3.613 0 0 0 1.465-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.387-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.782.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.543 1.465.635.308 1.437.462 2.406.462zm-.088-1.986c-.555 0-.993-.19-1.316-.568-.322-.38-.483-1.002-.483-1.87 0-.874.162-1.501.486-1.88.325-.38.754-.569 1.29-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556zm9.266 1.986c.828 0 1.544-.096 2.148-.29.603-.193 1.298-.557 2.083-1.092V8.102h-4.102v1.787h1.776v.808a4.59 4.59 0 0 1-.926.44 2.78 2.78 0 0 1-.85.129c-.625 0-1.112-.2-1.462-.598-.35-.398-.524-1.063-.524-1.992 0-.875.173-1.515.518-1.92.346-.403.81-.606 1.392-.606.39 0 .712.086.964.258.252.172.43.42.536.744l2.56-.457c-.156-.554-.39-1.012-.7-1.374a2.765 2.765 0 0 0-1.172-.8c-.47-.171-1.18-.257-2.13-.257-.984 0-1.767.138-2.349.416a3.757 3.757 0 0 0-1.7 1.579c-.386.693-.58 1.509-.58 2.446 0 .89.177 1.68.531 2.367a3.569 3.569 0 0 0 1.494 1.553c.643.348 1.474.521 2.493.521zm9.214 0c.953 0 1.749-.178 2.388-.536a3.613 3.613 0 0 0 1.464-1.5c.338-.642.507-1.465.507-2.47 0-1.382-.386-2.457-1.16-3.225-.773-.768-1.875-1.151-3.305-1.151-1.394 0-2.482.39-3.263 1.172-.781.78-1.172 1.873-1.172 3.275 0 1.004.197 1.84.592 2.508.394.668.909 1.156 1.544 1.465.634.308 1.436.462 2.405.462zm-.088-1.986c-.555 0-.993-.19-1.315-.568-.323-.38-.484-1.002-.484-1.87 0-.874.162-1.501.487-1.88.324-.38.753-.569 1.289-.569.558 0 1 .187 1.327.56.326.373.489.964.489 1.772 0 .961-.156 1.627-.469 1.999-.312.37-.754.556-1.324.556z" class="logo"/> + </g> + <rect width="214" height="1" x="10" y="29" fill="#FFF" class="rectangle_ver" opacity=".375"/> + <g fill="#FFF" class="group" opacity=".804" transform="translate(64 39)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + <g class="link" transform="translate(28)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(56)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <g class="link" transform="translate(84)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + </g> + <g fill="#FFF" class="group" opacity=".804" transform="translate(10 17)"> + <rect width="23" height="4" class="rectangle" rx="1"/> + </g> + <rect width="20" height="9" fill="#FFF" class="rectangle" rx="4.5" transform="translate(204 12)"/> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/image_left.svg b/addons/website/static/src/img/snippets_options/image_left.svg new file mode 100644 index 00000000..66fc80d9 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/image_left.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="11" viewBox="0 0 24 11"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_5" transform="translate(-137 -5)"> + <g class="image_left" transform="translate(137 5)"> + <rect width="8" height="11" x="16" fill="#B8B8B8" class="o_subdle"/> + <path fill="#FFF" d="M0 0h13v11H0V0zm1 1h11v9H1V1zm3.438 2.286c0 .357-.119.66-.356.91s-.525.375-.863.375c-.339 0-.627-.125-.864-.375A1.275 1.275 0 0 1 2 3.286c0-.357.118-.661.355-.911S2.88 2 3.22 2c.338 0 .626.125.863.375s.356.554.356.91zm6.5 2.571v3H2V7.571L4.031 5.43 5.047 6.5l3.25-3.429 2.64 2.786z" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/image_right.svg b/addons/website/static/src/img/snippets_options/image_right.svg new file mode 100644 index 00000000..8cc4ddca --- /dev/null +++ b/addons/website/static/src/img/snippets_options/image_right.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="11" viewBox="0 0 24 11"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_5" transform="translate(-171 -5)"> + <g class="image_right" transform="translate(171 5)"> + <rect width="8" height="11" fill="#B8B8B8" class="o_subdle"/> + <path fill="#FFF" d="M11 0h13v11H11V0zm1 1h11v9H12V1zm3.438 2.286c0 .357-.119.66-.356.91s-.525.375-.863.375c-.339 0-.627-.125-.864-.375a1.275 1.275 0 0 1-.355-.91c0-.357.118-.661.355-.911S13.88 2 14.22 2c.338 0 .626.125.863.375s.355.554.355.91zm6.5 2.571v3H13V7.571l2.031-2.142L16.047 6.5l3.25-3.429 2.64 2.786z" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/media_layout_1_2.svg b/addons/website/static/src/img/snippets_options/media_layout_1_2.svg new file mode 100644 index 00000000..f83de7d4 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/media_layout_1_2.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_3" transform="translate(-170 -7)"> + <g class="media_layout_1_2" transform="translate(170 7)"> + <rect width="10" height="8" x="13" fill="#B8B8B8" class="o_subdle"/> + <rect width="11" height="8" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/media_layout_1_2_right.svg b/addons/website/static/src/img/snippets_options/media_layout_1_2_right.svg new file mode 100644 index 00000000..70519f1f --- /dev/null +++ b/addons/website/static/src/img/snippets_options/media_layout_1_2_right.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_6" transform="translate(-171 -7)"> + <g class="media_layout_1_2_right" transform="translate(171 7)"> + <rect width="10" height="8" fill="#B8B8B8" class="o_subdle"/> + <rect width="11" height="8" x="12" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/media_layout_1_3.svg b/addons/website/static/src/img/snippets_options/media_layout_1_3.svg new file mode 100644 index 00000000..475877a2 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/media_layout_1_3.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_3" transform="translate(-204 -7)"> + <g class="media_layout_1_3" transform="translate(204 7)"> + <rect width="14" height="8" x="9" fill="#B8B8B8" class="o_subdle"/> + <rect width="7" height="8" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/media_layout_1_3_right.svg b/addons/website/static/src/img/snippets_options/media_layout_1_3_right.svg new file mode 100644 index 00000000..215a3115 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/media_layout_1_3_right.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_6" transform="translate(-204 -7)"> + <g class="media_layout_1_3_right" transform="translate(204 7)"> + <rect width="14" height="8" fill="#B8B8B8" class="o_subdle"/> + <rect width="7" height="8" x="16" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/media_layout_1_4.svg b/addons/website/static/src/img/snippets_options/media_layout_1_4.svg new file mode 100644 index 00000000..dd6e24c7 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/media_layout_1_4.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_3" transform="translate(-137 -7)"> + <g class="media_layout_1_4" transform="translate(137 7)"> + <rect width="18" height="8" x="5" fill="#B8B8B8" class="o_subdle"/> + <rect width="3" height="8" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/media_layout_1_4_right.svg b/addons/website/static/src/img/snippets_options/media_layout_1_4_right.svg new file mode 100644 index 00000000..219233dc --- /dev/null +++ b/addons/website/static/src/img/snippets_options/media_layout_1_4_right.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="8" viewBox="0 0 23 8"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_6" transform="translate(-137 -7)"> + <g class="media_layout_1_4_right" transform="translate(137 7)"> + <rect width="18" height="8" fill="#B8B8B8" class="o_subdle"/> + <rect width="3" height="8" x="20" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/pos_bottom.svg b/addons/website/static/src/img/snippets_options/pos_bottom.svg new file mode 100644 index 00000000..f4a83988 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/pos_bottom.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_8" transform="translate(-240 -5)"> + <g class="pos_bottom" transform="translate(240 5)"> + <rect width="20" height="12" class="bg"/> + <polygon fill="#D8D8D8" points="9.426 2.444 6 2.444 6 5.556 9.426 5.556 9.426 8 14 4 9.426 0" class="o_graphic" transform="rotate(90 10 4)"/> + <rect width="16" height="1" x="2" y="10" fill="#D8D8D8" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/pos_left.svg b/addons/website/static/src/img/snippets_options/pos_left.svg new file mode 100644 index 00000000..446e392b --- /dev/null +++ b/addons/website/static/src/img/snippets_options/pos_left.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_8" transform="translate(-172 -5)"> + <g class="pos_left" transform="translate(172 5)"> + <rect width="20" height="12" class="bg"/> + <polygon fill="#D8D8D8" points="11.054 4.444 7 4.444 7 7.556 11.054 7.556 11.054 10 17 6 11.054 2" class="o_graphic" transform="matrix(-1 0 0 1 24 0)"/> + <rect width="1" height="12" x="4" fill="#D8D8D8" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/pos_right.svg b/addons/website/static/src/img/snippets_options/pos_right.svg new file mode 100644 index 00000000..8990d7cd --- /dev/null +++ b/addons/website/static/src/img/snippets_options/pos_right.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_8" transform="translate(-138 -5)"> + <g class="pos_right" transform="translate(138 5)"> + <rect width="20" height="12" class="bg"/> + <polygon fill="#D8D8D8" points="7.054 4.444 3 4.444 3 7.556 7.054 7.556 7.054 10 13 6 7.054 2" class="o_graphic"/> + <rect width="1" height="12" x="15" fill="#D8D8D8" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/pos_top.svg b/addons/website/static/src/img/snippets_options/pos_top.svg new file mode 100644 index 00000000..df5adfe1 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/pos_top.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_8" transform="translate(-204 -5)"> + <g class="pos_top" transform="translate(204 5)"> + <rect width="20" height="12" class="bg"/> + <polygon fill="#D8D8D8" points="9.426 5.444 6 5.444 6 8.556 9.426 8.556 9.426 11 14 7 9.426 3" class="o_graphic" transform="rotate(-90 10 7)"/> + <rect width="16" height="1" x="2" fill="#D8D8D8" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/shadow_in.svg b/addons/website/static/src/img/snippets_options/shadow_in.svg new file mode 100644 index 00000000..fe9e273d --- /dev/null +++ b/addons/website/static/src/img/snippets_options/shadow_in.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="12" viewBox="0 0 23 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_shadow" transform="translate(-142 -5)"> + <g class="shadow_in_s" transform="translate(142 5)"> + <rect width="23" height="12" class="rectangle"/> + <path d="M6.5 9.5V3H17l2-3h1v12H3v-1l3.5-1.5z" class="o_graphic"/> + <polygon points="3.5 .5 17.5 .5 16.5 2 5.5 2 5.5 9 3.5 10" class="o_subdle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/shadow_out.svg b/addons/website/static/src/img/snippets_options/shadow_out.svg new file mode 100644 index 00000000..ee49026c --- /dev/null +++ b/addons/website/static/src/img/snippets_options/shadow_out.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="12" viewBox="0 0 23 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_shadow" transform="translate(-185 -5)"> + <g class="shadow_out_s" transform="translate(185 5)"> + <rect width="23" height="12" class="rectangle"/> + <polygon points="21 3.2 21 12 8.308 12 6 10.9 18.692 10.9 18.692 1" class="o_subdle"/> + <rect width="15" height="10" x="2" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/size_large.svg b/addons/website/static/src/img/snippets_options/size_large.svg new file mode 100644 index 00000000..13541780 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/size_large.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="12" viewBox="0 0 23 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_7" transform="translate(-203 -5)"> + <g class="size_large" transform="translate(203 5)"> + <path fill="#B8B8B8" d="M23 0v12H0V0h23zm-1 1H1v10h21V1z" class="o_subdle"/> + <rect width="19" height="8" x="2" y="2" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/size_medium.svg b/addons/website/static/src/img/snippets_options/size_medium.svg new file mode 100644 index 00000000..00b3a3d4 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/size_medium.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="12" viewBox="0 0 23 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_7" transform="translate(-170 -5)"> + <g class="size_medium" transform="translate(170 5)"> + <path fill="#B8B8B8" d="M23 0v12H0V0h23zm-4 2H4v8h15V2z" class="o_subdle"/> + <rect width="13" height="6" x="5" y="3" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_options/size_small.svg b/addons/website/static/src/img/snippets_options/size_small.svg new file mode 100644 index 00000000..aaa36a67 --- /dev/null +++ b/addons/website/static/src/img/snippets_options/size_small.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="23" height="12" viewBox="0 0 23 12"> + <g fill="none" fill-rule="evenodd" class="symbols"> + <g class="3_buttons_copy_7" transform="translate(-137 -5)"> + <g class="size_small" transform="translate(137 5)"> + <path fill="#B8B8B8" d="M23 0v12H0V0h23zm-8 3H8v6h7V3z" class="o_subdle"/> + <rect width="5" height="4" x="9" y="4" fill="#FFF" class="o_graphic"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/newsletter_subscribe_popup.svg b/addons/website/static/src/img/snippets_thumbs/newsletter_subscribe_popup.svg new file mode 100644 index 00000000..e903fb09 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/newsletter_subscribe_popup.svg @@ -0,0 +1,62 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="40.816%" y2="59.184%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="15" height="2" x="23" y="10"/> + <filter id="filter-3" width="106.7%" height="200%" x="-3.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-4" d="M3 11v10a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V11a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1zm7.445 3.63L5 11h12l-5.445 3.63a1 1 0 0 1-1.11 0zM4 21v-9.5l6.428 4.485a1 1 0 0 0 1.144 0L18 11.5V21H4z"/> + <filter id="filter-5" width="106.2%" height="116.7%" x="-3.1%" y="-4.2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-6" d="M49 23.571V22.43a.55.55 0 0 0-.17-.402.55.55 0 0 0-.401-.17h-2.286v-2.286a.55.55 0 0 0-.17-.401.55.55 0 0 0-.402-.17H44.43a.55.55 0 0 0-.402.17.55.55 0 0 0-.17.401v2.286h-2.286a.55.55 0 0 0-.401.17.55.55 0 0 0-.17.402v1.142a.55.55 0 0 0 .17.402.55.55 0 0 0 .401.17h2.286v2.286a.55.55 0 0 0 .17.401.55.55 0 0 0 .402.17h1.142a.55.55 0 0 0 .402-.17.55.55 0 0 0 .17-.401v-2.286h2.286a.55.55 0 0 0 .401-.17.55.55 0 0 0 .17-.402zM52 23c0 1.27-.313 2.441-.939 3.514a6.969 6.969 0 0 1-2.547 2.547A6.848 6.848 0 0 1 45 30a6.848 6.848 0 0 1-3.514-.939 6.969 6.969 0 0 1-2.547-2.547A6.848 6.848 0 0 1 38 23c0-1.27.313-2.441.939-3.514a6.969 6.969 0 0 1 2.547-2.547A6.848 6.848 0 0 1 45 16c1.27 0 2.441.313 3.514.939a6.969 6.969 0 0 1 2.547 2.547A6.848 6.848 0 0 1 52 23z"/> + <filter id="filter-8" width="107.1%" height="114.3%" x="-3.6%" y="-3.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-9" d="M31 18v1h-8v-1h8zm2-3v1H23v-1h10z"/> + <filter id="filter-10" width="110%" height="150%" x="-5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="newsletter_subscribe_popup"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 15)"> + <g fill="url(#linearGradient-1)" class="image_1" opacity=".4" transform="translate(0 6)"> + <rect width="49" height="21" class="rectangle"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-4"/> + </g> + <mask id="mask-7" fill="#fff"> + <use xlink:href="#path-6"/> + </mask> + <g class="plus_circle"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-6"/> + </g> + <path fill="#FFF" fill-opacity=".78" d="M49 0v6H0V0h49zm-1.538 1L46 2.461 44.538 1l-.53.537 1.459 1.46L44 4.464l.529.537L46 3.53 47.471 5 48 4.463l-1.467-1.465 1.458-1.461L47.462 1z" class="combined_shape"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-9"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_alert.svg b/addons/website/static/src/img/snippets_thumbs/s_alert.svg new file mode 100644 index 00000000..c6a1acbd --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_alert.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="22" height="2" x="3" y="3"/> + <filter id="filter-3" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-4" d="M19 11v1H3v-1h16zm4-3v1H3V8h20z"/> + <filter id="filter-5" width="105%" height="150%" x="-2.5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_alert"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(19 22)"> + <rect width="44" height="17" fill="url(#linearGradient-1)" fill-opacity=".4" class="rectangle_2" rx="1"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_badge.svg b/addons/website/static/src/img/snippets_thumbs/s_badge.svg new file mode 100644 index 00000000..27bab86e --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_badge.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="12" height="2" x="10" y="3"/> + <filter id="filter-3" width="108.3%" height="200%" x="-4.2%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <circle id="path-4" cx="4" cy="4" r="2"/> + <filter id="filter-5" width="125%" height="150%" x="-12.5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_badge"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(28 26)"> + <rect width="27" height="8" fill="url(#linearGradient-1)" class="rectangle" opacity=".4" rx="1"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="oval"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_banner.svg b/addons/website/static/src/img/snippets_thumbs/s_banner.svg new file mode 100644 index 00000000..276923f8 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_banner.svg @@ -0,0 +1,41 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="23.23%" y2="76.77%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="21" height="2" x="3" y="3"/> + <filter id="filter-3" width="104.8%" height="200%" x="-2.4%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.259587944 0 0 0 0 0.259629577 0 0 0 0 0.259574831 0 0 0 0.525895979 0"/> + </filter> + <path id="path-4" d="M18 11v1H3v-1h15zm2-3v1H3V8h17z"/> + <filter id="filter-5" width="105.9%" height="150%" x="-2.9%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_banner"> + <rect width="82" height="60" class="bg"/> + <g fill="url(#linearGradient-1)" class="group" opacity=".4"> + <g class="image_1"> + <rect width="82" height="60" class="rectangle"/> + </g> + </g> + <g class="group" transform="translate(13 22)"> + <rect width="28" height="16" fill="#FFF" class="rectangle"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#000" fill-opacity=".697" xlink:href="#path-2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_blockquote.svg b/addons/website/static/src/img/snippets_thumbs/s_blockquote.svg new file mode 100644 index 00000000..d583795e --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_blockquote.svg @@ -0,0 +1,37 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="12" height="2" x="11" y="0"/> + <filter id="filter-2" width="108.3%" height="200%" x="-4.2%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <linearGradient id="linearGradient-3" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-4" d="M32 8v1H11V8h21zm-5-3v1H11V5h16z"/> + <filter id="filter-5" width="104.8%" height="150%" x="-2.4%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_blockquote"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(20 24)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <path fill="url(#linearGradient-3)" d="M1.706 5c.349 0 .652-.114.909-.342C2.872 4.43 3 4.164 3 3.86c0-.147-.024-.29-.071-.427a.98.98 0 0 0-.243-.377 1.215 1.215 0 0 0-.472-.278 2.383 2.383 0 0 0-.755-.1h-.42c.05-.494.235-.914.555-1.26.319-.347.76-.651 1.324-.912L2.588 0A4.93 4.93 0 0 0 .76 1.318C.253 1.897 0 2.472 0 3.04c0 .613.147 1.092.441 1.44.295.346.716.519 1.265.519zm4 0c.349 0 .652-.114.909-.342C6.872 4.43 7 4.164 7 3.86c0-.147-.024-.29-.071-.427a.98.98 0 0 0-.243-.377 1.215 1.215 0 0 0-.472-.278 2.383 2.383 0 0 0-.755-.1h-.42c.05-.494.235-.914.555-1.26.319-.347.76-.651 1.324-.912L6.588 0A4.93 4.93 0 0 0 4.76 1.318C4.253 1.897 4 2.472 4 3.04c0 .613.147 1.092.441 1.44.295.346.716.519 1.265.519z"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-4"/> + </g> + <path fill="url(#linearGradient-3)" d="M36.411 12a4.93 4.93 0 0 0 1.83-1.318c.506-.579.759-1.154.759-1.723 0-.613-.147-1.092-.441-1.44-.295-.346-.716-.519-1.265-.519-.349 0-.652.114-.909.342-.257.228-.385.494-.385.798 0 .147.024.29.071.427a.98.98 0 0 0 .243.377c.12.12.277.212.472.278.194.067.446.1.755.1h.42c-.05.494-.235.914-.555 1.26-.319.347-.76.651-1.324.912l.33.506zm4 0a4.93 4.93 0 0 0 1.83-1.318c.506-.579.759-1.154.759-1.723 0-.613-.147-1.092-.441-1.44-.295-.346-.716-.519-1.265-.519-.349 0-.652.114-.909.342-.257.228-.385.494-.385.798 0 .147.024.29.071.427a.98.98 0 0 0 .243.377c.12.12.277.212.472.278.194.067.446.1.755.1h.42c-.05.494-.235.914-.555 1.26-.319.347-.76.651-1.324.912l.33.506z"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_call_to_action.svg b/addons/website/static/src/img/snippets_thumbs/s_call_to_action.svg new file mode 100644 index 00000000..8fd18dfa --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_call_to_action.svg @@ -0,0 +1,53 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="44.17%" y2="55.83%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="22" height="2" x="0" y="0"/> + <filter id="filter-3" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-4" d="M16 8v1H0V8h16zm4-3v1H0V5h20z"/> + <filter id="filter-5" width="105%" height="150%" x="-2.5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + <path id="path-6" d="M53.22 11.566c.15.145.183.312.1.501-.081.193-.224.29-.427.29h-2.771l1.458 3.453a.47.47 0 0 1-.247.61l-1.284.544a.47.47 0 0 1-.61-.247l-1.385-3.279-2.263 2.263a.446.446 0 0 1-.5.102c-.194-.082-.291-.225-.291-.428V4.465c0-.204.097-.347.29-.429a.45.45 0 0 1 .5.102l7.43 7.428z"/> + <filter id="filter-8" width="112%" height="115.4%" x="-6%" y="-3.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_call_to_action"> + <rect width="82" height="60" class="bg"/> + <g fill="url(#linearGradient-1)" class="group" opacity=".4" transform="translate(0 16)"> + <g class="image_1"> + <rect width="82" height="28" class="rectangle"/> + </g> + </g> + <g class="center_group" transform="translate(15 25)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-4"/> + </g> + <rect width="16" height="7" x="36.5" y=".5" fill="#1C1C1C" stroke="#FFF" class="rectangle" opacity=".703"/> + <mask id="mask-7" fill="#fff"> + <use xlink:href="#path-6"/> + </mask> + <g fill-rule="nonzero" class="mouse_pointer"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-6"/> + <use fill="#FFF" xlink:href="#path-6"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_card.svg b/addons/website/static/src/img/snippets_thumbs/s_card.svg new file mode 100644 index 00000000..69e95a9e --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_card.svg @@ -0,0 +1,32 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M44 4v14H0V4h44zM25 7H3v2h22V7z"/> + <filter id="filter-2" width="102.3%" height="114.3%" x="-1.1%" y="-3.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <rect id="path-3" width="20" height="1" x="3" y="11"/> + <filter id="filter-4" width="105%" height="300%" x="-2.5%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_card"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(19 21)"> + <g class="rectangle_2"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-1"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <rect width="44" height="4" fill="#FFF" fill-opacity=".78" class="combined_shape"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_carousel.svg b/addons/website/static/src/img/snippets_thumbs/s_carousel.svg new file mode 100644 index 00000000..aa1b9576 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_carousel.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="82" height="60" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="44.674%" y2="25.975%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="38.481%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="15" height="2" x="13" y="2"/> + <filter id="filter-6" width="106.7%" height="200%" x="-3.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.259587944 0 0 0 0 0.259629577 0 0 0 0 0.259574831 0 0 0 0.525895979 0"/> + </filter> + <path id="path-7" d="M25 10v1H13v-1h12zm5-3v1H13V7h17z"/> + <filter id="filter-8" width="105.9%" height="150%" x="-2.9%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-9" d="M28 30v2h-5v-2h5zm14 0v2h-5v-2h5zm7 0v2h-5v-2h5zm-14 0v2h-5v-2h5z"/> + <filter id="filter-10" width="103.8%" height="200%" x="-1.9%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_carousel"> + <rect width="82" height="60" class="bg"/> + <g class="group"> + <g class="oval___oval_mask" opacity=".5"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <circle cx="65.5" cy="11.5" r="7.5" fill="#F3EC60" class="oval" mask="url(#mask-2)"/> + <ellipse cx="68.5" cy="62" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="26.5" ry="20"/> + <ellipse cx="18" cy="67" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="51" ry="32"/> + </g> + <g class="center_group" transform="translate(6 20)"> + <rect width="21" height="14" x="11" fill="#FFF" class="rectangle"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#000" fill-opacity=".697" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-7"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-9"/> + </g> + <path fill="#FFF" stroke="#FFF" d="M3.5 2.648v9.704L-.659 7.5 3.5 2.648zm64-1L71.659 6.5 67.5 11.352V1.648z" class="combined_shape"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_channel.svg b/addons/website/static/src/img/snippets_thumbs/s_channel.svg new file mode 100644 index 00000000..0e4f3d32 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_channel.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M12 0a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1.292L7.844 4.11a2 2 0 0 1-2.168.05L1 1.29V8h11V1.292zM11.426 1h-9.67L5.64 3.534a2 2 0 0 0 2.245-.04L11.425 1z"/> + <filter id="filter-2" width="107.7%" height="122.2%" x="-3.8%" y="-5.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M43 0v9H16V0h27zm-1 1H17v7h25V1z"/> + <filter id="filter-4" width="103.7%" height="122.2%" x="-1.9%" y="-5.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <linearGradient id="linearGradient-5" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_channel"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(12 26)"> + <g class="shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-3"/> + </g> + <path fill="url(#linearGradient-5)" d="M57 3.556c0 .643-.245 1.238-.734 1.784-.49.547-1.158.978-2.004 1.295-.847.318-1.767.476-2.762.476-.448 0-.906-.037-1.375-.111a6.978 6.978 0 0 1-2.172.889 7.31 7.31 0 0 1-.672.111h-.023a.262.262 0 0 1-.16-.056.224.224 0 0 1-.09-.145.128.128 0 0 1-.008-.046c0-.016.001-.03.004-.045a.136.136 0 0 1 .016-.041l.02-.035.026-.038.032-.035.035-.035.031-.03c.026-.029.086-.087.18-.175.093-.088.161-.156.203-.204l.176-.202a1.75 1.75 0 0 0 .195-.267c.055-.093.108-.195.16-.306-.646-.333-1.154-.743-1.523-1.229-.37-.486-.555-1.005-.555-1.555 0-.644.245-1.239.734-1.785.49-.546 1.158-.978 2.004-1.295A7.816 7.816 0 0 1 51.5 0c.995 0 1.915.159 2.762.476.846.317 1.514.749 2.004 1.295.49.546.734 1.141.734 1.785zm3 1.558c0 .61-.181 1.182-.543 1.714-.363.531-.86.98-1.493 1.347.051.122.104.234.157.336.054.102.118.2.191.294l.173.221c.04.053.107.128.199.225a5.34 5.34 0 0 1 .271.302.344.344 0 0 1 .027.042l.02.038.015.046.003.05-.007.049a.283.283 0 0 1-.1.168.231.231 0 0 1-.168.053 6.45 6.45 0 0 1-.658-.122 6.444 6.444 0 0 1-2.128-.977 7.745 7.745 0 0 1-1.347.122c-1.382 0-2.586-.336-3.612-1.007a8.104 8.104 0 0 0 3.038-.313 6.782 6.782 0 0 0 2.02-.985c.638-.468 1.128-1.007 1.47-1.618A3.91 3.91 0 0 0 57.865 2c.658.361 1.178.814 1.561 1.359.383.544.574 1.13.574 1.755z" class="comments"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_chart.svg b/addons/website/static/src/img/snippets_thumbs/s_chart.svg new file mode 100644 index 00000000..f5c06dd2 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_chart.svg @@ -0,0 +1,46 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <linearGradient id="linearGradient-2" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <linearGradient id="linearGradient-3" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <linearGradient id="linearGradient-5" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-6" width="22" height="2" x="0" y="0"/> + <filter id="filter-7" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_chart"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(22 22)"> + <rect width="6" height="12" y="5" fill="url(#linearGradient-1)" class="rectangle" rx="1"/> + <rect width="6" height="7" x="8" y="10" fill="url(#linearGradient-2)" class="rectangle" rx="1"/> + <rect width="6" height="9" x="16" y="8" fill="url(#linearGradient-3)" class="rectangle" rx="1"/> + <rect width="6" height="4" x="25" y="13" fill="url(#linearGradient-4)" class="rectangle" rx="1"/> + <rect width="6" height="13" x="33" y="4" fill="url(#linearGradient-5)" class="rectangle" rx="1"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-7)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-6"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_color_blocks_2.svg b/addons/website/static/src/img/snippets_thumbs/s_color_blocks_2.svg new file mode 100644 index 00000000..48b7b20c --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_color_blocks_2.svg @@ -0,0 +1,66 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="27.778%" x2="72.222%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00E2FF"/> + <stop offset="100%" stop-color="#00A09D"/> + </linearGradient> + <linearGradient id="linearGradient-2" x1="27.778%" x2="72.222%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <linearGradient id="linearGradient-3" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-4" d="M27 50.429V52H13v-1.571h14zm-1.556-4.715v1.572h-9.333v-1.572h9.333zM27 41v1.571H13V41h14z"/> + <filter id="filter-5" width="107.1%" height="118.2%" x="-3.6%" y="-4.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + <rect id="path-6" width="27" height="3" x="7" y="33"/> + <filter id="filter-7" width="103.7%" height="166.7%" x="-1.9%" y="-16.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-8" d="M69 50.429V52H55v-1.571h14zm-1.556-4.715v1.572h-9.333v-1.572h9.333zM69 41v1.571H55V41h14z"/> + <filter id="filter-9" width="107.1%" height="118.2%" x="-3.6%" y="-4.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + <rect id="path-10" width="27" height="3" x="49" y="33"/> + <filter id="filter-11" width="103.7%" height="166.7%" x="-1.9%" y="-16.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_color_blocks_2"> + <rect width="82" height="60" class="bg"/> + <g class="group"> + <path fill="url(#linearGradient-1)" d="M82 0v60H42V0h40zM63 6c-5.523 0-10 4.494-10 10.038 0 5.544 4.477 10.038 10 10.038s10-4.494 10-10.038C73 10.494 68.523 6 63 6z" class="combined_shape" opacity=".4"/> + <path fill="url(#linearGradient-2)" d="M40 0v60H0V0h40zM20 6c-5.523 0-10 4.494-10 10.038 0 5.544 4.477 10.038 10 10.038s10-4.494 10-10.038C30 10.494 25.523 6 20 6z" class="combined_shape" opacity=".4"/> + <path fill="url(#linearGradient-3)" d="M20 7a9 9 0 1 1 0 18 9 9 0 0 1 0-18zm43 0a9 9 0 1 1 0 18 9 9 0 0 1 0-18z" class="combined_shape"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-4"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-7)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-6"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-9)" xlink:href="#path-8"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-8"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-11)" xlink:href="#path-10"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-10"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_company_team.svg b/addons/website/static/src/img/snippets_thumbs/s_company_team.svg new file mode 100644 index 00000000..c62a9e65 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_company_team.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M43 16v2H16v-2h27zm5-16v2H16V0h32z"/> + <filter id="filter-2" width="103.1%" height="111.1%" x="-1.6%" y="-2.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M28 24v1H16v-1h12zm13-3v1H16v-1h25zM39 8v1H16V8h23zm7-3v1H16V5h30z"/> + <filter id="filter-4" width="103.3%" height="110%" x="-1.7%" y="-2.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <linearGradient id="linearGradient-5" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_company_team"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(17 17)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <path fill="url(#linearGradient-5)" d="M5 16a5 5 0 1 1 0 10 5 5 0 0 1 0-10zM5 0a5 5 0 1 1 0 10A5 5 0 0 1 5 0z" class="combined_shape"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_comparisons.svg b/addons/website/static/src/img/snippets_thumbs/s_comparisons.svg new file mode 100644 index 00000000..8178f99e --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_comparisons.svg @@ -0,0 +1,73 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="45.918%" y2="54.082%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <linearGradient id="linearGradient-2" x1="0%" x2="100%" y1="42.969%" y2="57.031%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-3" d="M12 9v2H3V9h9zm37 0v2h-9V9h9z"/> + <filter id="filter-4" width="102.2%" height="200%" x="-1.1%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-5" d="M8 22v1H3v-1h5zm4-2v1H3v-1h9zm-3-3v1H3v-1h6zm2-3v1H3v-1h8z"/> + <filter id="filter-6" width="111.1%" height="122.2%" x="-5.6%" y="-5.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-7" d="M27 20v1h-7v-1h7zm5-3v1H20v-1h12zm-4-3v1h-8v-1h8zm3-3v1H20v-1h11z"/> + <filter id="filter-8" width="108.3%" height="120%" x="-4.2%" y="-5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-9" d="M49 22v1h-9v-1h9zm-5-2v1h-4v-1h4zm4-3v1h-8v-1h8zm-2-3v1h-6v-1h6z"/> + <filter id="filter-10" width="111.1%" height="122.2%" x="-5.6%" y="-5.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-11" width="12" height="2" x="20" y="6"/> + <filter id="filter-12" width="108.3%" height="200%" x="-4.2%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_comparisons"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 12)"> + <path fill="#D8D8D8" d="M14 3a1 1 0 0 1 1 1v29a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h13zm0 4H1v26h13V7zm37-4a1 1 0 0 1 1 1v29a1 1 0 0 1-1 1H38a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h13zm0 4H38v26h13V7zM34 0a1 1 0 0 1 1 1v34a1 1 0 0 1-1 1H18a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h16zm0 4H18v31h16V4z" class="combined_shape"/> + <rect width="7" height="2" x="4" y="28" fill="url(#linearGradient-1)" class="rectangle"/> + <rect width="7" height="2" x="41" y="28" fill="url(#linearGradient-1)" class="rectangle"/> + <rect width="8" height="3" x="22" y="29" fill="url(#linearGradient-2)" class="rectangle"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-9"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-11"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_countdown.svg b/addons/website/static/src/img/snippets_thumbs/s_countdown.svg new file mode 100644 index 00000000..cb7a9980 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_countdown.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1=".245%" y2="99.755%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_countdown"> + <rect width="82" height="60" class="bg"/> + <g class="countdown" transform="translate(24 12)"> + <path fill="url(#linearGradient-1)" d="M17 5.396a14.66 14.66 0 0 1 10.71 4.628l-2.02 1.244A12.34 12.34 0 0 0 17 7.708c-6.828 0-12.364 5.522-12.364 12.334 0 6.811 5.536 12.333 12.364 12.333 6.828 0 12.364-5.522 12.364-12.333 0-.398.036-1.154 0-1.542h2.238c.053.507.08 1.021.08 1.542 0 8.088-6.573 14.645-14.682 14.645S2.318 28.13 2.318 20.043c0-8.09 6.573-14.647 14.682-14.647z" class="icon_fore"/> + <path fill="#84848C" d="M18.546 29.945V37h-3.091v-7.055a10.147 10.147 0 0 0 3.09 0zM26.927 18.5H34v3.083h-7.073a10.073 10.073 0 0 0 0-3.083zM6.955 20.042c0 .524.04 1.04.118 1.542H0V18.5h7.073a10.073 10.073 0 0 0-.118 1.542zM19.09 0a1 1 0 0 1 1 1v2.854h-1.546v6.285a10.147 10.147 0 0 0-3.09 0V3.854h-1.546V1a1 1 0 0 1 1-1h4.182z" class="combined_shape"/> + <path fill="#FFF" fill-opacity=".78" d="M17 30.063c5.548 0 10.045-4.487 10.045-10.021 0-5.535-4.497-10.021-10.045-10.021S6.955 14.507 6.955 20.04c0 5.535 4.497 10.021 10.045 10.021zm2.438-10.036a2.3 2.3 0 0 1 .633.554c.175.222.31.477.405.766.095.288.142.598.142.928 0 .445-.09.844-.27 1.199-.18.354-.438.657-.775.907a3.77 3.77 0 0 1-1.215.582 5.733 5.733 0 0 1-1.593.206c-1.44 0-2.568-.421-3.383-1.263l1.222-1.617c.256.265.548.466.875.603.327.137.718.206 1.173.206 1.07 0 1.606-.392 1.606-1.177 0-.426-.159-.747-.476-.965-.318-.217-.818-.326-1.5-.326h-.427V19h.47c.53 0 .945-.11 1.244-.327a1.02 1.02 0 0 0 .447-.865c0-.312-.109-.549-.327-.71-.218-.16-.507-.24-.867-.24a2.07 2.07 0 0 0-1.059.276 2.356 2.356 0 0 0-.803.787l-1.436-1.645c.151-.199.35-.383.597-.553.246-.17.526-.314.839-.433a6.138 6.138 0 0 1 2.119-.376c.501 0 .961.06 1.378.178.417.118.773.288 1.066.51.294.223.522.487.683.795.16.307.242.65.242 1.028 0 .284-.045.553-.135.808a2.47 2.47 0 0 1-.377.695c-.161.208-.353.38-.576.518a2.06 2.06 0 0 1-.732.276c.294.057.564.159.81.305z" class="xmlid_900__copy_2"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_cover.svg b/addons/website/static/src/img/snippets_thumbs/s_cover.svg new file mode 100644 index 00000000..6fb7174f --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_cover.svg @@ -0,0 +1,40 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="23.23%" y2="76.77%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="21" height="2" x="0" y="0"/> + <filter id="filter-3" width="104.8%" height="200%" x="-2.4%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-4" d="M18 8v1H3V8h15zm1-3v1H2V5h17z"/> + <filter id="filter-5" width="105.9%" height="150%" x="-2.9%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_cover"> + <rect width="82" height="60" class="bg"/> + <g fill="url(#linearGradient-1)" class="group" opacity=".4"> + <g class="image_1"> + <rect width="82" height="60" class="rectangle"/> + </g> + </g> + <g class="center_group" transform="translate(31 26)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_dynamic_carousel.svg b/addons/website/static/src/img/snippets_thumbs/s_dynamic_carousel.svg new file mode 100644 index 00000000..d47d35d8 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_dynamic_carousel.svg @@ -0,0 +1,76 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M27 30v2h-5v-2h5zm14 0v2h-5v-2h5zm7 0v2h-5v-2h5zm-14 0v2h-5v-2h5z"/> + <filter id="filter-2" width="103.8%" height="200%" x="-1.9%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-3" width="20.208" height="13.176" x="0" y="0"/> + <linearGradient id="linearGradient-5" x1="72.875%" x2="40.332%" y1="47.143%" y2="37.112%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-6" x1="88.517%" x2="50%" y1="47.295%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-7" width="20.208" height="13.176" x="0" y="0"/> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_dynamic_carousel"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(6 14)"> + <g class="center_group" transform="translate(0 9)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <path fill="#FFF" stroke="#FFF" d="M3.5-.352v9.704L-.659 4.5 3.5-.352zm64-1L71.659 3.5 67.5 8.352v-9.704z" class="combined_shape"/> + </g> + <g class="group_2" transform="translate(38)"> + <g class="image_1_border"> + <rect width="21" height="14" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.396 .412)"> + <mask id="mask-4" fill="#fff"> + <use xlink:href="#path-3"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-3"/> + <ellipse cx="16.164" cy="4.049" fill="#F3EC60" class="oval" mask="url(#mask-4)" rx="2.044" ry="2.049"/> + <ellipse cx="20.009" cy="14.618" fill="url(#linearGradient-5)" class="oval" mask="url(#mask-4)" rx="9.311" ry="5.147"/> + <ellipse cx="-12.622" cy="14" fill="url(#linearGradient-6)" class="oval" mask="url(#mask-4)" rx="24.378" ry="7.412"/> + </g> + <path fill="#FFF" d="M21 0v14H0V0h21zm-.396.412H.396v13.176h20.208V.412z" class="rectangle_2"/> + </g> + <path fill="#B9B9B9" d="M21 13v18H0V13h21zm-1 1H1v16h19V14z" class="combined_shape"/> + </g> + <g class="group_2" transform="translate(10)"> + <g class="image_1_border"> + <rect width="21" height="14" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.396 .412)"> + <mask id="mask-8" fill="#fff"> + <use xlink:href="#path-7"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-7"/> + <ellipse cx="16.164" cy="4.049" fill="#F3EC60" class="oval" mask="url(#mask-8)" rx="2.044" ry="2.049"/> + <ellipse cx="20.009" cy="14.618" fill="url(#linearGradient-5)" class="oval" mask="url(#mask-8)" rx="9.311" ry="5.147"/> + <ellipse cx="-12.622" cy="14" fill="url(#linearGradient-6)" class="oval" mask="url(#mask-8)" rx="24.378" ry="7.412"/> + </g> + <path fill="#FFF" d="M21 0v14H0V0h21zm-.396.412H.396v13.176h20.208V.412z" class="rectangle_2"/> + </g> + <path fill="#B9B9B9" d="M21 13v18H0V13h21zm-1 1H1v16h19V14z" class="combined_shape"/> + </g> + <g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8" class="database" transform="translate(43 18)"> + <ellipse cx="5.5" cy="1.5" class="oval" rx="5.5" ry="1.5"/> + <path d="M11 4c0 1.107-2.444 2-5.5 2S0 5.107 0 4" class="path"/> + <path d="M0 2v5.765C0 8.448 2.444 9 5.5 9S11 8.448 11 7.765V2" class="path"/> + </g> + <g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8" class="database" transform="translate(15 18)"> + <ellipse cx="5.5" cy="1.5" class="oval" rx="5.5" ry="1.5"/> + <path d="M11 4c0 1.107-2.444 2-5.5 2S0 5.107 0 4" class="path"/> + <path d="M0 2v5.765C0 8.448 2.444 9 5.5 9S11 8.448 11 7.765V2" class="path"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_dynamic_snippet.svg b/addons/website/static/src/img/snippets_thumbs/s_dynamic_snippet.svg new file mode 100644 index 00000000..30321cbf --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_dynamic_snippet.svg @@ -0,0 +1,85 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="20.208" height="13.176" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="47.143%" y2="37.112%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="47.295%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="20.208" height="13.176" x="0" y="0"/> + <rect id="path-7" width="20.208" height="13.176" x="0" y="0"/> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_dynamic_snippet"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(5 14)"> + <g class="group_2" transform="translate(52)"> + <g class="image_1_border"> + <rect width="21" height="14" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.396 .412)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="16.164" cy="4.049" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="2.044" ry="2.049"/> + <ellipse cx="20.009" cy="14.618" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="9.311" ry="5.147"/> + <ellipse cx="-12.622" cy="14" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="24.378" ry="7.412"/> + </g> + <path fill="#FFF" d="M21 0v14H0V0h21zm-.396.412H.396v13.176h20.208V.412z" class="rectangle_2"/> + </g> + <path fill="#B9B9B9" d="M21 13v18H0V13h21zm-1 1H1v16h19V14z" class="combined_shape"/> + </g> + <g class="group_2"> + <g class="image_1_border"> + <rect width="21" height="14" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.396 .412)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-5"/> + <ellipse cx="16.164" cy="4.049" fill="#F3EC60" class="oval" mask="url(#mask-6)" rx="2.044" ry="2.049"/> + <ellipse cx="20.009" cy="14.618" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-6)" rx="9.311" ry="5.147"/> + <ellipse cx="-12.622" cy="14" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-6)" rx="24.378" ry="7.412"/> + </g> + <path fill="#FFF" d="M21 0v14H0V0h21zm-.396.412H.396v13.176h20.208V.412z" class="rectangle_2"/> + </g> + <path fill="#B9B9B9" d="M21 13v18H0V13h21zm-1 1H1v16h19V14z" class="combined_shape"/> + </g> + <g class="group_2" transform="translate(26)"> + <g class="image_1_border"> + <rect width="21" height="14" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.396 .412)"> + <mask id="mask-8" fill="#fff"> + <use xlink:href="#path-7"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-7"/> + <ellipse cx="16.164" cy="4.049" fill="#F3EC60" class="oval" mask="url(#mask-8)" rx="2.044" ry="2.049"/> + <ellipse cx="20.009" cy="14.618" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-8)" rx="9.311" ry="5.147"/> + <ellipse cx="-12.622" cy="14" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-8)" rx="24.378" ry="7.412"/> + </g> + <path fill="#FFF" d="M21 0v14H0V0h21zm-.396.412H.396v13.176h20.208V.412z" class="rectangle_2"/> + </g> + <path fill="#B9B9B9" d="M21 13v18H0V13h21zm-1 1H1v16h19V14z" class="combined_shape"/> + </g> + <g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8" class="database" transform="translate(57 18)"> + <ellipse cx="5.5" cy="1.5" class="oval" rx="5.5" ry="1.5"/> + <path d="M11 4c0 1.107-2.444 2-5.5 2S0 5.107 0 4" class="path"/> + <path d="M0 2v5.765C0 8.448 2.444 9 5.5 9S11 8.448 11 7.765V2" class="path"/> + </g> + <g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8" class="database" transform="translate(5 18)"> + <ellipse cx="5.5" cy="1.5" class="oval" rx="5.5" ry="1.5"/> + <path d="M11 4c0 1.107-2.444 2-5.5 2S0 5.107 0 4" class="path"/> + <path d="M0 2v5.765C0 8.448 2.444 9 5.5 9S11 8.448 11 7.765V2" class="path"/> + </g> + <g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round" stroke-width=".8" class="database" transform="translate(31 18)"> + <ellipse cx="5.5" cy="1.5" class="oval" rx="5.5" ry="1.5"/> + <path d="M11 4c0 1.107-2.444 2-5.5 2S0 5.107 0 4" class="path"/> + <path d="M0 2v5.765C0 8.448 2.444 9 5.5 9S11 8.448 11 7.765V2" class="path"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_facebook_page.svg b/addons/website/static/src/img/snippets_thumbs/s_facebook_page.svg new file mode 100644 index 00000000..daf20630 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_facebook_page.svg @@ -0,0 +1,41 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M52 0v17H0V0h52zM14.391 2H4.61a.59.59 0 0 0-.43.179.586.586 0 0 0-.179.43v9.782c0 .167.06.31.179.43.12.12.263.179.43.179h5.263V8.739H8.44V7.077h1.432V5.853c0-.697.195-1.237.584-1.619.39-.382.908-.573 1.558-.573a9.6 9.6 0 0 1 1.274.065v1.482l-.873.007c-.325 0-.543.067-.656.201-.112.134-.168.334-.168.602v1.06h1.64l-.215 1.66h-1.425V13h2.8a.59.59 0 0 0 .43-.179.586.586 0 0 0 .179-.43V2.61a.59.59 0 0 0-.179-.43.586.586 0 0 0-.43-.179z"/> + <filter id="filter-2" width="101.9%" height="111.8%" x="-1%" y="-2.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <rect id="path-3" width="22" height="2" x="21" y="3"/> + <filter id="filter-4" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.259587944 0 0 0 0 0.259629577 0 0 0 0 0.259574831 0 0 0 0.525895979 0"/> + </filter> + <path id="path-5" d="M37 11v1H21v-1h16zm4-3v1H21V8h20z"/> + <filter id="filter-6" width="105%" height="150%" x="-2.5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_facebook_page"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 22)"> + <g class="rectangle_2"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-1"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#000" fill-opacity=".697" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-5"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_faq_collapse.svg b/addons/website/static/src/img/snippets_thumbs/s_faq_collapse.svg new file mode 100644 index 00000000..ea9bff39 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_faq_collapse.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-2" d="M38 8v1H8V8h30zm-7-3v1H8V5h23zm12-3v1H8V2h35z"/> + <filter id="filter-3" width="102.9%" height="128.6%" x="-1.4%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-4" width="26" height="1" x="8" y="17"/> + <filter id="filter-5" width="103.8%" height="300%" x="-1.9%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-6" width="33" height="1" x="8" y="24"/> + <filter id="filter-7" width="103%" height="300%" x="-1.5%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-8" width="28" height="1" x="8" y="31"/> + <filter id="filter-9" width="103.6%" height="300%" x="-1.8%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_faq_collapse"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 13)"> + <rect width="53" height="13" fill="#D8D8D8" class="rectangle" opacity=".219"/> + <rect width="53" height="5" y="16" fill="#D8D8D8" class="rectangle" opacity=".219"/> + <rect width="53" height="5" y="23" fill="#D8D8D8" class="rectangle" opacity=".219"/> + <rect width="53" height="5" y="30" fill="#D8D8D8" class="rectangle" opacity=".219"/> + <rect width="3" height="3" x="2" y="31" fill="url(#linearGradient-1)" class="rectangle" rx="1"/> + <rect width="3" height="3" x="2" y="24" fill="url(#linearGradient-1)" class="rectangle" rx="1"/> + <rect width="3" height="3" x="2" y="17" fill="url(#linearGradient-1)" class="rectangle" rx="1"/> + <rect width="3" height="3" x="2" y="2" fill="url(#linearGradient-1)" class="rectangle" rx="1"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-2"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-4"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-7)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-6"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-9)" xlink:href="#path-8"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-8"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_features.svg b/addons/website/static/src/img/snippets_thumbs/s_features.svg new file mode 100644 index 00000000..fcfa1fe7 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_features.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-2" d="M15 16v2H0v-2h15zm19 0v2H19v-2h15zm19 0v2H38v-2h15z"/> + <filter id="filter-3" width="101.9%" height="200%" x="-.9%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-4" d="M14 28v1H0v-1h14zm19 0v1H19v-1h14zm19 0v1H38v-1h14zm-41-3v1H0v-1h11zm19 0v1H19v-1h11zm19 0v1H38v-1h11zm-35-3v1H0v-1h14zm19 0v1H19v-1h14zm19 0v1H38v-1h14z"/> + <filter id="filter-5" width="101.9%" height="128.6%" x="-1%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_features"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 13)"> + <path fill="url(#linearGradient-1)" d="M7 0a6 6 0 1 1 0 12A6 6 0 0 1 7 0zm19 0a6 6 0 1 1 0 12 6 6 0 0 1 0-12zm19 0a6 6 0 1 1 0 12 6 6 0 0 1 0-12z" class="combined_shape"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_features_grid.svg b/addons/website/static/src/img/snippets_thumbs/s_features_grid.svg new file mode 100644 index 00000000..874ef6c0 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_features_grid.svg @@ -0,0 +1,65 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="21.12" height="2" x="0" y="0"/> + <filter id="filter-2" width="104.7%" height="200%" x="-2.4%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M19 20v1H8v-1h11zm2-2v1H8v-1h13zm-5-9v1H8V9h8zm5-2v1H8V7h13z"/> + <filter id="filter-4" width="107.7%" height="114.3%" x="-3.8%" y="-3.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <linearGradient id="linearGradient-5" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-6" width="24.038" height="2" x="0" y="0"/> + <filter id="filter-7" width="104.2%" height="200%" x="-2.1%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-8" d="M19 20v1H8v-1h11zm3-2v1H8v-1h14zm-3-9v1H8V9h11zm-1-2v1H8V7h10z"/> + <filter id="filter-9" width="107.1%" height="114.3%" x="-3.6%" y="-3.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_features_grid"> + <rect width="82" height="60" class="bg"/> + <g class="group_3" transform="translate(15 18)"> + <g class="group_2"> + <g class="group"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + </g> + <path fill="url(#linearGradient-5)" d="M3 18a3 3 0 1 1 0 6 3 3 0 0 1 0-6zM3 7a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" class="combined_shape"/> + </g> + <g class="group_2" transform="translate(28)"> + <g class="group"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-7)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-6"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-9)" xlink:href="#path-8"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-8"/> + </g> + </g> + <path fill="url(#linearGradient-5)" d="M3 18a3 3 0 1 1 0 6 3 3 0 0 1 0-6zM3 7a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" class="combined_shape"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_google_map.svg b/addons/website/static/src/img/snippets_thumbs/s_google_map.svg new file mode 100644 index 00000000..e69fb2d2 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_google_map.svg @@ -0,0 +1,30 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="23.231%" y2="76.769%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-2" d="M10 6.5c0-.966-.342-1.791-1.025-2.475A3.372 3.372 0 0 0 6.5 3c-.966 0-1.791.342-2.475 1.025A3.372 3.372 0 0 0 3 6.5c0 .966.342 1.791 1.025 2.475A3.372 3.372 0 0 0 6.5 10c.966 0 1.791-.342 2.475-1.025A3.372 3.372 0 0 0 10 6.5zm3 .167c0 .946-.14 1.723-.419 2.33L7.96 19.076a1.566 1.566 0 0 1-.603.677 1.6 1.6 0 0 1-1.714 0 1.488 1.488 0 0 1-.59-.677L.419 8.997C.139 8.39 0 7.613 0 6.667c0-1.84.635-3.412 1.904-4.714C3.174.651 4.706 0 6.5 0s3.326.651 4.596 1.953S13 4.826 13 6.667z"/> + <filter id="filter-4" width="107.7%" height="110%" x="-3.8%" y="-2.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_google_map"> + <g fill="url(#linearGradient-1)" class="image_1" opacity=".4"> + <path d="M23.033 36.864L32.38 60H0V44.348l23.033-7.484zM60.346 24.74L74.592 60H55.05L43.071 30.353l17.275-5.613zM82 17.705V60h-4.567L62.858 23.924 82 17.704zM40.56 31.169l11.65 28.83H35.22l-9.677-23.951 15.015-4.88zM27.966-.001L39.57 28.722 0 41.579V0h27.966zM50.35 0l9.006 22.293-17.274 5.613L30.807 0H50.35zM82 0v14.935l-20.132 6.54L53.191 0H82z" class="combined_shape"/> + </g> + <g class="group" transform="translate(17 14)"> + <mask id="mask-3" fill="#fff"> + <use xlink:href="#path-2"/> + </mask> + <g class="map_marker"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_google_map_marker.png b/addons/website/static/src/img/snippets_thumbs/s_google_map_marker.png Binary files differnew file mode 100644 index 00000000..cbfec358 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_google_map_marker.png diff --git a/addons/website/static/src/img/snippets_thumbs/s_hr.svg b/addons/website/static/src/img/snippets_thumbs/s_hr.svg new file mode 100644 index 00000000..b9197424 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_hr.svg @@ -0,0 +1,22 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="44" height="1" x="0" y="5.5"/> + <filter id="filter-2" width="102.3%" height="300%" x="-1.1%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_hr"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(19 24)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <path fill="#FFF" stroke="#FFF" d="M25.925 10.5L22 13.64l-3.925-3.14h7.85zm0-8h-7.85L22-.64l3.925 3.14z" class="combined_shape"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_image_gallery.svg b/addons/website/static/src/img/snippets_thumbs/s_image_gallery.svg new file mode 100644 index 00000000..3dceedfb --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_image_gallery.svg @@ -0,0 +1,32 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="82" height="60" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="44.674%" y2="25.975%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="38.481%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_image_gallery"> + <rect width="82" height="60" class="bg"/> + <g class="group" opacity=".5"> + <g class="oval___oval_mask"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <circle cx="65.5" cy="11.5" r="7.5" fill="#F3EC60" class="oval" mask="url(#mask-2)"/> + <ellipse cx="68.5" cy="62" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="26.5" ry="20"/> + <ellipse cx="18" cy="67" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="51" ry="32"/> + </g> + </g> + <g fill="#FFF" stroke="#FFF" class="center_group" transform="translate(9 27)"> + <path d="M61.5-1.352L65.659 3.5 61.5 8.352v-9.704zm-58 0v9.704L-.659 3.5 3.5-1.352z" class="combined_shape"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_image_text.svg b/addons/website/static/src/img/snippets_thumbs/s_image_text.svg new file mode 100644 index 00000000..9353ed75 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_image_text.svg @@ -0,0 +1,53 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="23.077" height="18.116" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="46.509%" y2="34.249%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="39.469%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="25" height="2" x="28" y="0"/> + <filter id="filter-6" width="104%" height="200%" x="-2%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-7" d="M51 12v1H28v-1h23zm-4-3v1H28V9h19zm4-3v1H28V6h23z"/> + <filter id="filter-8" width="104.3%" height="128.6%" x="-2.2%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_image_text"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 21)"> + <g class="image_1_border"> + <rect width="24" height="19" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.462 .442)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="17.769" cy="4.64" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="3.462" ry="3.314"/> + <ellipse cx="23.308" cy="19.884" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="10.846" ry="6.628"/> + <ellipse cx=".231" cy="20.105" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="17.308" ry="10.384"/> + </g> + <path fill="#FFF" d="M24 0v19H0V0h24zm-1 1H1v17h22V1z" class="rectangle_2"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_images_wall.svg b/addons/website/static/src/img/snippets_thumbs/s_images_wall.svg new file mode 100644 index 00000000..22acc70d --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_images_wall.svg @@ -0,0 +1,91 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="24.057" height="15.059" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="47.367%" y2="38.122%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="46.899%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="24.057" height="15.059" x="0" y="0"/> + <rect id="path-7" width="12.5" height="9.535" x="0" y="0"/> + <linearGradient id="linearGradient-9" x1="72.875%" x2="40.332%" y1="46.704%" y2="35.129%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-10" x1="88.517%" x2="50%" y1="40.057%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-11" width="12.5" height="10.488" x="0" y="0"/> + <linearGradient id="linearGradient-13" x1="72.875%" x2="40.332%" y1="46.011%" y2="32.006%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-14" x1="88.517%" x2="50%" y1="37.969%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_images_wall"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(21 13)"> + <g class="image_1_border"> + <rect width="25" height="16" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.472 .47)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="20.519" cy="4.394" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="2.594" ry="2.394"/> + <ellipse cx="23.821" cy="16.706" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="11.085" ry="5.882"/> + <ellipse cx="-10.982" cy="16" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="26.018" ry="8.471"/> + </g> + <path fill="#FFF" d="M25 0v16H0V0h25zm-.472.47H.472v15.06h24.056V.47z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(15 18)"> + <rect width="25" height="16" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.472 .47)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-5"/> + <ellipse cx="20.519" cy="4.394" fill="#F3EC60" class="oval" mask="url(#mask-6)" rx="2.594" ry="2.394"/> + <ellipse cx="23.821" cy="16.706" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-6)" rx="11.085" ry="5.882"/> + <ellipse cx="-10.982" cy="16" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-6)" rx="26.018" ry="8.471"/> + </g> + <path fill="#FFF" d="M25 0v16H0V0h25zm-.472.47H.472v15.06h24.056V.47z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(27)"> + <rect width="13" height="10" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.25 .233)"> + <mask id="mask-8" fill="#fff"> + <use xlink:href="#path-7"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-7"/> + <ellipse cx="9.625" cy="2.442" fill="#F3EC60" class="oval" mask="url(#mask-8)" rx="1.875" ry="1.744"/> + <ellipse cx="12.625" cy="10.465" fill="url(#linearGradient-9)" class="oval" mask="url(#mask-8)" rx="5.875" ry="3.488"/> + <ellipse cx=".125" cy="10.581" fill="url(#linearGradient-10)" class="oval" mask="url(#mask-8)" rx="9.375" ry="5.465"/> + </g> + <path fill="#FFF" d="M13 0v10H0V0h13zm-1 1H1v8h11V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(0 18)"> + <rect width="13" height="11" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.25 .256)"> + <mask id="mask-12" fill="#fff"> + <use xlink:href="#path-11"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-11"/> + <ellipse cx="9.625" cy="2.686" fill="#F3EC60" class="oval" mask="url(#mask-12)" rx="1.875" ry="1.919"/> + <ellipse cx="12.625" cy="11.512" fill="url(#linearGradient-13)" class="oval" mask="url(#mask-12)" rx="5.875" ry="3.837"/> + <ellipse cx=".125" cy="11.64" fill="url(#linearGradient-14)" class="oval" mask="url(#mask-12)" rx="9.375" ry="6.012"/> + </g> + <path fill="#FFF" d="M13 0v11H0V0h13zm-1 1H1v9h11V1z" class="rectangle_2"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_masonry_block.svg b/addons/website/static/src/img/snippets_thumbs/s_masonry_block.svg new file mode 100644 index 00000000..36c6e1b8 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_masonry_block.svg @@ -0,0 +1,126 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="33.944%" y2="66.056%"> + <stop offset="0%" stop-color="#00E2FF"/> + <stop offset="100%" stop-color="#00A09D"/> + </linearGradient> + <linearGradient id="linearGradient-2" x1="0%" x2="100%" y1="31.569%" y2="68.431%"> + <stop offset="0%" stop-color="#00E2FF"/> + <stop offset="100%" stop-color="#00A09D"/> + </linearGradient> + <path id="path-3" d="M20 12v1H4v-1h16zm-6-3v1H4V9h10zm6-3v1H4V6h16z"/> + <filter id="filter-4" width="106.2%" height="128.6%" x="-3.1%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + <rect id="path-5" width="21" height="1" x="4" y="3"/> + <filter id="filter-6" width="104.8%" height="300%" x="-2.4%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-7" d="M52 29v1H34v-1h18zm-6-3v1H34v-1h12zm6-3v1H34v-1h18z"/> + <filter id="filter-8" width="105.6%" height="128.6%" x="-2.8%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + <rect id="path-9" width="19" height="1" x="34" y="19"/> + <filter id="filter-10" width="105.3%" height="300%" x="-2.6%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-11" d="M52 12v1H34v-1h18zm-6-3v1H34V9h12zm6-3v1H34V6h18z"/> + <filter id="filter-12" width="105.6%" height="128.6%" x="-2.8%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-13" width="21" height="1" x="34" y="3"/> + <filter id="filter-14" width="104.8%" height="300%" x="-2.4%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-15" d="M20 29v1H4v-1h16zm-6-3v1H4v-1h10zm6-3v1H4v-1h16z"/> + <filter id="filter-16" width="106.2%" height="128.6%" x="-3.1%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-17" width="15" height="1" x="4" y="19"/> + <filter id="filter-18" width="106.7%" height="300%" x="-3.3%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-19" width="23.442" height="33" x="0" y="0"/> + <linearGradient id="linearGradient-21" x1="72.875%" x2="40.332%" y1="46.301%" y2="33.313%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-22" x1="88.517%" x2="50%" y1="38.842%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_masonry_block"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(0 15)"> + <g class="group_2" transform="translate(24)"> + <rect width="58" height="33" fill="#D8D8D8" class="rectangle" opacity=".058"/> + <rect width="30" height="17" fill="url(#linearGradient-1)" class="rectangle" opacity=".4"/> + <rect width="28" height="17" x="30" y="16" fill="url(#linearGradient-2)" class="rectangle" opacity=".4"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-3"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-7"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-9"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-14)" xlink:href="#path-13"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-13"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-16)" xlink:href="#path-15"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-15"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-18)" xlink:href="#path-17"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-17"/> + </g> + </g> + <g class="image_1_border"> + <rect width="24" height="33" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask"> + <mask id="mask-20" fill="#fff"> + <use xlink:href="#path-19"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-19"/> + <ellipse cx="16.465" cy="6.325" fill="#F3EC60" class="oval" mask="url(#mask-20)" rx="4.186" ry="4.125"/> + <ellipse cx="19.256" cy="34.1" fill="url(#linearGradient-21)" class="oval" mask="url(#mask-20)" rx="13.116" ry="8.25"/> + <ellipse cx="-8.651" cy="34.375" fill="url(#linearGradient-22)" class="oval" mask="url(#mask-20)" rx="20.93" ry="12.925"/> + </g> + <path fill="#FFF" d="M24 0v33H0V0h24zm-1 1H1v31h22V1z" class="rectangle_2"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_media_list.svg b/addons/website/static/src/img/snippets_thumbs/s_media_list.svg new file mode 100644 index 00000000..c059743f --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_media_list.svg @@ -0,0 +1,90 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M32 15v1H21v-1h11zm-4-3v1h-7v-1h7zm4-3v1H21V9h11z"/> + <filter id="filter-2" width="109.1%" height="128.6%" x="-4.5%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-3" width="23" height="2" x="21" y="5"/> + <filter id="filter-4" width="104.3%" height="200%" x="-2.2%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.259587944 0 0 0 0 0.259629577 0 0 0 0 0.259574831 0 0 0 0.525895979 0"/> + </filter> + <path id="path-5" d="M42 33v1H21v-1h21zm-7-3v1H21v-1h14zm7-3v1H21v-1h21z"/> + <filter id="filter-6" width="104.8%" height="128.6%" x="-2.4%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-7" width="13" height="2" x="21" y="23"/> + <filter id="filter-8" width="107.7%" height="200%" x="-3.8%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.259587944 0 0 0 0 0.259629577 0 0 0 0 0.259574831 0 0 0 0.525895979 0"/> + </filter> + <rect id="path-9" width="17.308" height="14.302" x="0" y="0"/> + <linearGradient id="linearGradient-11" x1="72.875%" x2="40.332%" y1="46.131%" y2="32.548%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-12" x1="88.517%" x2="50%" y1="38.331%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-13" width="17.308" height="14.302" x="0" y="0"/> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_media_list"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 11)"> + <rect width="53" height="39" class="rectangle"/> + <rect width="36" height="15" x="17" y="21" fill="#FFF" class="rectangle"/> + <rect width="36" height="15" x="17" y="3" fill="#FFF" class="rectangle"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-1"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#000" fill-opacity=".697" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#000" fill-opacity=".697" xlink:href="#path-7"/> + </g> + <g class="image_1_border" transform="translate(0 3)"> + <rect width="18" height="15" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.346 .349)"> + <mask id="mask-10" fill="#fff"> + <use xlink:href="#path-9"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-9"/> + <ellipse cx="13.327" cy="3.663" fill="#F3EC60" class="oval" mask="url(#mask-10)" rx="2.596" ry="2.616"/> + <ellipse cx="17.481" cy="15.698" fill="url(#linearGradient-11)" class="oval" mask="url(#mask-10)" rx="8.135" ry="5.233"/> + <ellipse cx=".173" cy="15.872" fill="url(#linearGradient-12)" class="oval" mask="url(#mask-10)" rx="12.981" ry="8.198"/> + </g> + <path fill="#FFF" d="M18 0v15H0V0h18zm-1 1H1v13h16V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(0 21)"> + <rect width="18" height="15" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.346 .349)"> + <mask id="mask-14" fill="#fff"> + <use xlink:href="#path-13"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-13"/> + <ellipse cx="13.327" cy="3.663" fill="#F3EC60" class="oval" mask="url(#mask-14)" rx="2.596" ry="2.616"/> + <ellipse cx="17.481" cy="15.698" fill="url(#linearGradient-11)" class="oval" mask="url(#mask-14)" rx="8.135" ry="5.233"/> + <ellipse cx=".173" cy="15.872" fill="url(#linearGradient-12)" class="oval" mask="url(#mask-14)" rx="12.981" ry="8.198"/> + </g> + <path fill="#FFF" d="M18 0v15H0V0h18zm-1 1H1v13h16V1z" class="rectangle_2"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_mega_menu_menu_image_menu.svg b/addons/website/static/src/img/snippets_thumbs/s_mega_menu_menu_image_menu.svg new file mode 100644 index 00000000..2dd08e55 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_mega_menu_menu_image_menu.svg @@ -0,0 +1,73 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="13" height="2" x="0" y="0"/> + <filter id="filter-2" width="107.7%" height="200%" x="-3.8%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M12 12v1H0v-1h12zm-2-3v1H0V9h10zm2-3v1H0V6h12z"/> + <filter id="filter-4" width="108.3%" height="128.6%" x="-4.2%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-5" width="15.059" height="19.07" x="0" y="0"/> + <linearGradient id="linearGradient-7" x1="72.875%" x2="40.332%" y1="46.279%" y2="33.212%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-8" x1="88.517%" x2="50%" y1="38.775%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-9" width="14" height="2" x="39" y="0"/> + <filter id="filter-10" width="107.1%" height="200%" x="-3.6%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-11" d="M52 12v1H39v-1h13zm-2-3v1H39V9h11zm2-3v1H39V6h13z"/> + <filter id="filter-12" width="107.7%" height="128.6%" x="-3.8%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_mega_menu_menu_image_menu"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 20)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="image_1_border" transform="translate(19)"> + <rect width="16" height="20" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.47 .465)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-5"/> + <ellipse cx="9.647" cy="4.884" fill="#F3EC60" class="oval" mask="url(#mask-6)" rx="3.529" ry="3.488"/> + <ellipse cx="15.294" cy="20.93" fill="url(#linearGradient-7)" class="oval" mask="url(#mask-6)" rx="11.059" ry="6.977"/> + <ellipse cx="-8.235" cy="21.163" fill="url(#linearGradient-8)" class="oval" mask="url(#mask-6)" rx="17.647" ry="10.93"/> + </g> + <path fill="#FFF" d="M16 0v20H0V0h16zm-1 1H1v18h14V1z" class="rectangle_2"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-9"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_mega_menu_multi_menus.svg b/addons/website/static/src/img/snippets_thumbs/s_mega_menu_multi_menus.svg new file mode 100644 index 00000000..57244e37 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_mega_menu_multi_menus.svg @@ -0,0 +1,71 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="14" height="2" x="0" y="0"/> + <filter id="filter-2" width="107.1%" height="200%" x="-3.6%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M13 12v1H0v-1h13zm-2-3v1H0V9h11zm2-3v1H0V6h13z"/> + <filter id="filter-4" width="107.7%" height="128.6%" x="-3.8%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-5" width="14" height="2" x="20" y="0"/> + <filter id="filter-6" width="107.1%" height="200%" x="-3.6%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-7" d="M33 12v1H20v-1h13zm-2-3v1H20V9h11zm2-3v1H20V6h13z"/> + <filter id="filter-8" width="107.7%" height="128.6%" x="-3.8%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-9" width="14" height="2" x="39" y="0"/> + <filter id="filter-10" width="107.1%" height="200%" x="-3.6%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-11" d="M52 12v1H39v-1h13zm-2-3v1H39V9h11zm2-3v1H39V6h13z"/> + <filter id="filter-12" width="107.7%" height="128.6%" x="-3.8%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_mega_menu_multi_menus"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 24)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-9"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_newsletter_subscribe_form.svg b/addons/website/static/src/img/snippets_thumbs/s_newsletter_subscribe_form.svg new file mode 100644 index 00000000..152e61f8 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_newsletter_subscribe_form.svg @@ -0,0 +1,40 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M12 0a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1.292L7.844 4.11a2 2 0 0 1-2.168.05L1 1.29V8h11V1.292zM11.426 1h-9.67L5.64 3.534a2 2 0 0 0 2.245-.04L11.425 1z"/> + <filter id="filter-2" width="107.7%" height="122.2%" x="-3.8%" y="-5.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M37 0v9H16V0h21zm-.677 1H16.677v7h19.646V1z"/> + <filter id="filter-4" width="104.8%" height="122.2%" x="-2.4%" y="-5.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <linearGradient id="linearGradient-5" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-6" d="M45.5 9a.377.377 0 0 1-.27-.105L41.4 5.367a1.991 1.991 0 0 1-.17-.152 6.108 6.108 0 0 1-.34-.384 5.188 5.188 0 0 1-.417-.571 3.47 3.47 0 0 1-.329-.71A2.461 2.461 0 0 1 40 2.743c0-.86.26-1.531.78-2.015C41.3.242 42.017 0 42.934 0c.254 0 .513.042.777.126.264.084.51.197.736.34.227.142.423.276.586.401.164.125.32.258.467.399.147-.141.303-.274.467-.399.163-.125.359-.259.586-.401.227-.143.472-.256.736-.34.264-.084.523-.126.777-.126.917 0 1.635.242 2.154.727.52.484.78 1.156.78 2.015 0 .863-.469 1.742-1.406 2.637L45.77 8.895A.377.377 0 0 1 45.5 9z"/> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_newsletter_subscribe_form"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(16 26)"> + <g class="shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-3"/> + </g> + <mask id="mask-7" fill="#fff"> + <use xlink:href="#path-6"/> + </mask> + <use fill="url(#linearGradient-5)" class="heart" xlink:href="#path-6"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_numbers.svg b/addons/website/static/src/img/snippets_thumbs/s_numbers.svg new file mode 100644 index 00000000..49c90148 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_numbers.svg @@ -0,0 +1,53 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M14 26v1H5v-1h9zm-1-3v1H7v-1h6zm1-3v1H5v-1h9z"/> + <filter id="filter-2" width="111.1%" height="128.6%" x="-5.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-3" width="17" height="2" x="1" y="14"/> + <filter id="filter-4" width="105.9%" height="200%" x="-2.9%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-5" d="M40 26v1h-9v-1h9zm-1-3v1h-6v-1h6zm1-3v1h-9v-1h9z"/> + <filter id="filter-6" width="111.1%" height="128.6%" x="-5.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-7" width="17" height="2" x="27" y="14"/> + <filter id="filter-8" width="105.9%" height="200%" x="-2.9%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_numbers"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(19 16)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-1"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-7"/> + </g> + <path fill="#E4E4E4" fill-rule="nonzero" d="M7.41 10.522v-2.59h1.47V6.489H7.41V.46H5.504L.595 6.674v1.258h4.526v2.59h2.29zM5.12 6.49H1.642l3.48-4.395V6.49zm7.89 4.033c1.207 0 2.2-.317 2.976-.953.777-.636 1.166-1.455 1.166-2.458 0-.57-.116-1.047-.349-1.432a2.858 2.858 0 0 0-.861-.92 3.482 3.482 0 0 0-1.152-.502 5.439 5.439 0 0 0-1.2-.133c-.524 0-.98.07-1.367.212-.387.141-.704.29-.95.444l.28-2.256h5.284V.638h-5.776l-.663 5.03.533.179c.228-.278.486-.503.773-.674.287-.17.642-.256 1.066-.256.547 0 .992.206 1.336.619.345.412.517.94.517 1.582 0 .442-.053.838-.158 1.186-.104.349-.26.65-.465.906a1.803 1.803 0 0 1-1.408.684c-.127 0-.264-.01-.41-.031a1.814 1.814 0 0 1-.383-.092c.05-.142.114-.347.192-.616.077-.269.116-.517.116-.745a.964.964 0 0 0-.345-.776c-.23-.193-.526-.29-.886-.29-.346 0-.623.111-.83.335a1.16 1.16 0 0 0-.311.82c0 .547.312 1.02.936 1.422.625.4 1.404.601 2.338.601z" class="45"/> + <path fill="#E4E4E4" fill-rule="nonzero" d="M29.128 10.563c1.941-.255 3.473-.91 4.594-1.965 1.12-1.055 1.681-2.412 1.681-4.07 0-1.254-.353-2.252-1.06-2.995C33.638.79 32.686.42 31.487.42c-1.107 0-2.03.316-2.768.947-.739.63-1.108 1.427-1.108 2.389 0 .451.072.87.216 1.254.143.385.343.715.598.988.25.269.547.48.889.636.341.155.708.232 1.1.232.47 0 .892-.056 1.268-.167.376-.112.765-.343 1.166-.694-.064.406-.16.8-.287 1.183A4.04 4.04 0 0 1 32 8.274c-.237.333-.54.625-.91.875-.368.251-.833.456-1.394.616l-.724.123.157.676zM31.54 5.99c-.42 0-.758-.216-1.015-.65-.258-.432-.386-1.013-.386-1.742 0-.834.126-1.472.379-1.914.253-.442.582-.663.988-.663.42 0 .76.29 1.022.871s.393 1.419.393 2.512c0 .087-.002.176-.007.267a4.767 4.767 0 0 0-.007.232 2.9 2.9 0 0 1-.007.222 1.756 1.756 0 0 0-.006.12c-.192.287-.4.483-.623.588a1.7 1.7 0 0 1-.731.157zM42.574 8v-.52a7.37 7.37 0 0 1-.615-.082c-.278-.045-.476-.095-.595-.15a.69.69 0 0 1-.304-.277.831.831 0 0 1-.106-.427V2.538c0-.314.007-.67.02-1.066.014-.397.028-.743.042-1.04h-1.203a3.585 3.585 0 0 1-.298.363 2.502 2.502 0 0 1-.482.396 2.842 2.842 0 0 1-.721.315 3.61 3.61 0 0 1-1.029.13h-.376v.65h1.73v4.333c0 .192-.039.342-.116.451a.623.623 0 0 1-.322.233 3.68 3.68 0 0 1-.612.11c-.298.036-.516.058-.652.067V8h5.64z" class="91"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_parallax.svg b/addons/website/static/src/img/snippets_thumbs/s_parallax.svg new file mode 100644 index 00000000..aea38151 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_parallax.svg @@ -0,0 +1,52 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="82" height="60" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="44.674%" y2="25.975%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="38.481%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <path id="path-5" d="M13.5 34.998a.367.367 0 0 1-.13.277L7.3 40.88A.429.429 0 0 1 7 41a.429.429 0 0 1-.3-.12L.63 35.275a.367.367 0 0 1-.13-.277c0-.104.043-.196.13-.276l.652-.602a.429.429 0 0 1 .3-.12.43.43 0 0 1 .299.12L7 38.847l5.12-4.727a.429.429 0 0 1 .299-.12.43.43 0 0 1 .3.12l.65.602c.088.08.131.172.131.276z"/> + <path id="path-7" d="M13.5.998a.367.367 0 0 1-.13.277L7.3 6.88A.429.429 0 0 1 7 7a.429.429 0 0 1-.3-.12L.63 1.275A.367.367 0 0 1 .5.998C.5.894.543.802.63.722L1.282.12a.429.429 0 0 1 .3-.12.43.43 0 0 1 .299.12L7 4.847 12.12.12a.429.429 0 0 1 .299-.12.43.43 0 0 1 .3.12l.65.602c.088.08.131.172.131.276z"/> + <linearGradient id="linearGradient-9" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#FFF"/> + <stop offset="32.744%" stop-color="#F0F0F0"/> + <stop offset="100%" stop-color="#F3F3F3"/> + </linearGradient> + <linearGradient id="linearGradient-10" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#757575"/> + <stop offset="100%" stop-color="#414141"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_parallax"> + <rect width="82" height="60" class="bg"/> + <g class="group" opacity=".5"> + <g class="oval___oval_mask"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <circle cx="65.5" cy="11.5" r="7.5" fill="#F3EC60" class="oval" mask="url(#mask-2)"/> + <ellipse cx="68.5" cy="62" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="26.5" ry="20"/> + <ellipse cx="18" cy="67" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="51" ry="32"/> + </g> + </g> + <g class="center_group" transform="translate(34 10)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#FFF" fill-rule="nonzero" class="angle_down" xlink:href="#path-5"/> + <mask id="mask-8" fill="#fff"> + <use xlink:href="#path-7"/> + </mask> + <use fill="#FFF" fill-rule="nonzero" class="angle_down" transform="matrix(1 0 0 -1 0 7)" xlink:href="#path-7"/> + <rect width="13" height="20" x=".5" y="10.5" fill="url(#linearGradient-9)" stroke="#E4E4E4" class="rectangle" rx="5"/> + <rect width="2" height="5" x="6" y="12" fill="url(#linearGradient-10)" class="rectangle" rx="1"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_picture.svg b/addons/website/static/src/img/snippets_thumbs/s_picture.svg new file mode 100644 index 00000000..d33e7670 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_picture.svg @@ -0,0 +1,53 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="35.604" height="22.588" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="47.295%" y2="37.799%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="45.065%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <path id="path-5" d="M27 8v1H10V8h17zm-3-3v1H13V5h11z"/> + <filter id="filter-6" width="105.9%" height="150%" x="-2.9%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-7" width="29" height="2" x="4" y="0"/> + <filter id="filter-8" width="103.4%" height="200%" x="-1.7%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_picture"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(23 13)"> + <g class="image_1_border" transform="translate(0 11)"> + <rect width="37" height="24" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.698 .706)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="30.368" cy="5.775" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="3.84" ry="3.775"/> + <ellipse cx="35.255" cy="25.059" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="16.406" ry="8.824"/> + <ellipse cx="-6.061" cy="24" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="30.939" ry="12.706"/> + </g> + <path fill="#FFF" d="M37 0v24H0V0h37zm-.698.706H.698v22.588h35.604V.706z" class="rectangle_2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-7"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_popup.svg b/addons/website/static/src/img/snippets_thumbs/s_popup.svg new file mode 100644 index 00000000..e34e6f71 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_popup.svg @@ -0,0 +1,39 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="44.009%" y2="55.991%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="22" height="2" x="4" y="10"/> + <filter id="filter-3" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-4" d="M20 18v1H4v-1h16zm4-3v1H4v-1h20z"/> + <filter id="filter-5" width="105%" height="150%" x="-2.5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_popup"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 18)"> + <g fill="url(#linearGradient-1)" class="image_1" opacity=".4" transform="translate(0 6)"> + <rect width="52" height="18" class="rectangle"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <path fill="#FFF" fill-opacity=".78" d="M52 0v6H0V0h52zm-1.538 1L49 2.461 47.538 1l-.53.537 1.459 1.46L47 4.464l.529.537L49 3.53 50.471 5 51 4.463l-1.467-1.465 1.458-1.461L50.462 1z" class="combined_shape"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_process_steps.svg b/addons/website/static/src/img/snippets_thumbs/s_process_steps.svg new file mode 100644 index 00000000..d4117bdd --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_process_steps.svg @@ -0,0 +1,28 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="31" height="1" x="11" y="5"/> + <filter id="filter-2" width="103.2%" height="300%" x="-1.6%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <linearGradient id="linearGradient-3" x1="0%" x2="100%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_process_steps"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 25)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-1"/> + </g> + <circle cx="47.5" cy="5.5" r="5.5" fill="url(#linearGradient-3)" class="oval" opacity=".426"/> + <circle cx="5.5" cy="5.5" r="5.5" fill="#FFF" fill-opacity=".78" class="oval"/> + <circle cx="26.5" cy="5.5" r="5.5" fill="url(#linearGradient-3)" class="oval"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_product_catalog.svg b/addons/website/static/src/img/snippets_thumbs/s_product_catalog.svg new file mode 100644 index 00000000..1c0d8263 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_product_catalog.svg @@ -0,0 +1,61 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="37" height="2" x="0" y="0"/> + <filter id="filter-2" width="102.7%" height="200%" x="-1.4%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M17 27.813V29H0v-1.188h17zm0-5.938v1.188H0v-1.188h17zm0-5.938v1.188H0v-1.188h17zM17 10v1.188H0V10h17z"/> + <filter id="filter-4" width="105.9%" height="110.5%" x="-2.9%" y="-2.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-5" d="M24 27.813V29h-2v-1.188h2zm0-5.938v1.188h-2v-1.188h2zm0-5.938v1.188h-2v-1.188h2zM24 10v1.188h-2V10h2z"/> + <filter id="filter-6" width="150%" height="110.5%" x="-25%" y="-2.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-7" d="M51 27.813V29H34v-1.188h17zm0-5.938v1.188H34v-1.188h17zm0-5.938v1.188H34v-1.188h17zM51 10v1.188H34V10h17z"/> + <filter id="filter-8" width="105.9%" height="110.5%" x="-2.9%" y="-2.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-9" d="M58 27.813V29h-2v-1.188h2zm0-5.938v1.188h-2v-1.188h2zm0-5.938v1.188h-2v-1.188h2zM58 10v1.188h-2V10h2z"/> + <filter id="filter-10" width="150%" height="110.5%" x="-25%" y="-2.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_product_catalog"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(12 15)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-9"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_product_list.svg b/addons/website/static/src/img/snippets_thumbs/s_product_list.svg new file mode 100644 index 00000000..cc162fc3 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_product_list.svg @@ -0,0 +1,88 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <polygon id="path-2" points="0 10.447 6.5 13.16 6.5 5.5 0 3"/> + <filter id="filter-3" width="115.4%" height="119.7%" x="-7.7%" y="-4.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <polygon id="path-4" points="7.5 13.16 14 10.447 14 3 7.5 5.5"/> + <filter id="filter-5" width="115.4%" height="119.7%" x="-7.7%" y="-4.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <polygon id="path-6" points="0 10.447 6.5 13.16 6.5 5.5 0 3"/> + <filter id="filter-7" width="115.4%" height="119.7%" x="-7.7%" y="-4.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <polygon id="path-8" points="7.5 13.16 14 10.447 14 3 7.5 5.5"/> + <filter id="filter-9" width="115.4%" height="119.7%" x="-7.7%" y="-4.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <polygon id="path-10" points="0 10.447 6.5 13.16 6.5 5.5 0 3"/> + <filter id="filter-11" width="115.4%" height="119.7%" x="-7.7%" y="-4.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <polygon id="path-12" points="7.5 13.16 14 10.447 14 3 7.5 5.5"/> + <filter id="filter-13" width="115.4%" height="119.7%" x="-7.7%" y="-4.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_product_list"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 20)"> + <path fill="url(#linearGradient-1)" d="M13 18v2H1v-2h12zm20 0v2H20v-2h13zm20 0v2H38v-2h15z" class="combined_shape"/> + <g class="box_solid"> + <rect width="14" height="13" class="rectangle"/> + <polygon fill="#FFF" fill-opacity=".78" points="7 .5 0 2.405 7 5 14 2.405" class="path"/> + <g class="path"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="path"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-4"/> + </g> + </g> + <g class="box_solid" transform="translate(38)"> + <rect width="14" height="13" class="rectangle"/> + <polygon fill="#FFF" fill-opacity=".78" points="7 .5 0 2.405 7 5 14 2.405" class="path"/> + <g class="path"> + <use fill="#000" filter="url(#filter-7)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-6"/> + </g> + <g class="path"> + <use fill="#000" filter="url(#filter-9)" xlink:href="#path-8"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-8"/> + </g> + </g> + <g class="box_solid" transform="translate(19)"> + <rect width="14" height="13" class="rectangle"/> + <polygon fill="#FFF" fill-opacity=".78" points="7 .5 0 2.405 7 5 14 2.405" class="path"/> + <g class="path"> + <use fill="#000" filter="url(#filter-11)" xlink:href="#path-10"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-10"/> + </g> + <g class="path"> + <use fill="#000" filter="url(#filter-13)" xlink:href="#path-12"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-12"/> + </g> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_progress_bar.svg b/addons/website/static/src/img/snippets_thumbs/s_progress_bar.svg new file mode 100644 index 00000000..ecff924a --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_progress_bar.svg @@ -0,0 +1,27 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="22" height="2" x="0" y="0"/> + <filter id="filter-3" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_progress_bar"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(19 24)"> + <path fill="#FFF" fill-opacity=".641" d="M20 5h23a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H20V5z" class="rectangle" opacity=".433"/> + <path fill="url(#linearGradient-1)" d="M1 5h18v8H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z" class="rectangle"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_quotes_carousel.svg b/addons/website/static/src/img/snippets_thumbs/s_quotes_carousel.svg new file mode 100644 index 00000000..4c57f1bc --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_quotes_carousel.svg @@ -0,0 +1,47 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="0%" x2="100%" y1="44.17%" y2="55.83%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-2" d="M11.193 7c.449 0 .839-.16 1.169-.479.33-.319.495-.691.495-1.116 0-.207-.03-.406-.091-.599a1.391 1.391 0 0 0-.313-.528 1.541 1.541 0 0 0-.606-.39c-.25-.092-.574-.139-.971-.139h-.539c.064-.691.301-1.28.712-1.765.41-.485.978-.91 1.702-1.276L12.328 0a6.386 6.386 0 0 0-2.352 1.845C9.326 2.655 9 3.46 9 4.258c0 .857.19 1.529.568 2.014.378.485.92.728 1.625.728zm5.143 0c.449 0 .838-.16 1.169-.479.33-.319.495-.691.495-1.116 0-.207-.03-.406-.091-.599a1.391 1.391 0 0 0-.313-.528 1.541 1.541 0 0 0-.606-.39c-.25-.092-.574-.139-.971-.139h-.54c.065-.691.302-1.28.713-1.765.41-.485.978-.91 1.702-1.276L17.471 0a6.386 6.386 0 0 0-2.352 1.845c-.65.81-.976 1.615-.976 2.413 0 .857.189 1.529.567 2.014.379.485.92.728 1.626.728z"/> + <filter id="filter-3" width="111.1%" height="128.6%" x="-5.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-4" width="19" height="3" x="22" y="6"/> + <filter id="filter-5" width="105.3%" height="166.7%" x="-2.6%" y="-16.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-6" d="M47.549 14a6.593 6.593 0 0 0 2.439-1.845C50.663 11.345 51 10.54 51 9.742c0-.857-.196-1.529-.589-2.014C50.02 7.243 49.457 7 48.726 7c-.466 0-.87.16-1.212.479-.343.319-.514.691-.514 1.116 0 .207.032.406.095.599.063.193.171.369.324.528.16.166.369.296.628.39.26.092.596.139 1.008.139h.558c-.066.691-.312 1.28-.738 1.765-.425.485-1.014.91-1.765 1.276l.439.708zm5 0a6.593 6.593 0 0 0 2.439-1.845C55.663 11.345 56 10.54 56 9.742c0-.857-.196-1.529-.589-2.014C55.02 7.243 54.457 7 53.726 7c-.466 0-.87.16-1.212.479-.343.319-.514.691-.514 1.116 0 .207.032.406.095.599.063.193.171.369.324.528.16.166.369.296.628.39.26.092.596.139 1.008.139h.558c-.066.691-.312 1.28-.738 1.765-.425.485-1.014.91-1.765 1.276l.439.708z"/> + <filter id="filter-7" width="111.1%" height="128.6%" x="-5.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_quotes_carousel"> + <rect width="82" height="60" class="bg"/> + <g fill="url(#linearGradient-1)" class="group" opacity=".4" transform="translate(0 17)"> + <g class="image_1"> + <rect width="82" height="28" class="rectangle"/> + </g> + </g> + <g class="group" transform="translate(8 22)"> + <path fill="#FFF" stroke="#FFF" d="M62.5 3.648L66.659 8.5 62.5 13.352V3.648zm-59 0v9.704L-.659 8.5 3.5 3.648z" class="combined_shape"/> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-2"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-4"/> + </g> + <use fill="#000" filter="url(#filter-7)" xlink:href="#path-6"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-6"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_rating.svg b/addons/website/static/src/img/snippets_thumbs/s_rating.svg new file mode 100644 index 00000000..911434f4 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_rating.svg @@ -0,0 +1,47 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="22" height="2" x="0" y="0"/> + <filter id="filter-2" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-3" d="M43 8.1c0 .074-.047.155-.14.242l-1.964 1.785.465 2.52a.71.71 0 0 1 .006.101c0 .07-.02.13-.057.179-.038.049-.093.073-.165.073a.461.461 0 0 1-.217-.06L38.5 11.75l-2.428 1.19a.485.485 0 0 1-.217.06c-.076 0-.132-.024-.17-.073a.283.283 0 0 1-.057-.179c0-.02.004-.054.01-.1l.466-2.521-1.969-1.785c-.09-.09-.135-.171-.135-.242 0-.124.101-.201.303-.232l2.715-.368 1.217-2.293c.068-.138.157-.207.265-.207.108 0 .197.069.265.207L39.982 7.5l2.715.368c.202.03.303.108.303.232z"/> + <filter id="filter-4" width="111.1%" height="125%" x="-5.6%" y="-6.2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-5" d="M31 8.1c0 .074-.047.155-.14.242l-1.964 1.785.465 2.52a.71.71 0 0 1 .006.101c0 .07-.02.13-.057.179-.038.049-.093.073-.165.073a.461.461 0 0 1-.217-.06L26.5 11.75l-2.428 1.19a.485.485 0 0 1-.217.06c-.076 0-.132-.024-.17-.073a.283.283 0 0 1-.057-.179c0-.02.004-.054.01-.1l.466-2.521-1.969-1.785c-.09-.09-.135-.171-.135-.242 0-.124.101-.201.303-.232l2.715-.368 1.217-2.293c.068-.138.157-.207.265-.207.108 0 .197.069.265.207L27.982 7.5l2.715.368c.202.03.303.108.303.232z"/> + <filter id="filter-6" width="111.1%" height="125%" x="-5.6%" y="-6.2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <linearGradient id="linearGradient-7" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_rating"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(20 24)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-1"/> + </g> + <g class="mask"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="mask"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <path fill="url(#linearGradient-7)" d="M19 8.1c0 .074-.042.155-.125.242l-1.745 1.785.413 2.52a.799.799 0 0 1 .005.101c0 .07-.017.13-.05.179a.167.167 0 0 1-.147.073.376.376 0 0 1-.192-.06L15 11.75l-2.159 1.19a.395.395 0 0 1-.192.06c-.067 0-.118-.024-.151-.073a.307.307 0 0 1-.05-.179c0-.02.002-.054.009-.1l.413-2.521-1.75-1.785c-.08-.09-.12-.171-.12-.242 0-.124.09-.201.27-.232l2.413-.368 1.081-2.293c.061-.138.14-.207.236-.207.096 0 .175.069.236.207L16.317 7.5l2.414.368c.18.03.269.108.269.232z" class="star_copy"/> + <path fill="url(#linearGradient-7)" d="M8 8.1c0 .074-.042.155-.125.242L6.13 10.127l.413 2.52a.799.799 0 0 1 .005.101c0 .07-.017.13-.05.179A.167.167 0 0 1 6.35 13a.376.376 0 0 1-.192-.06L4 11.75l-2.159 1.19a.395.395 0 0 1-.192.06c-.067 0-.118-.024-.151-.073a.307.307 0 0 1-.05-.179c0-.02.002-.054.009-.1l.413-2.521L.12 8.342C.04 8.252 0 8.171 0 8.1c0-.124.09-.201.27-.232L2.682 7.5l1.081-2.293C3.825 5.069 3.904 5 4 5s.175.069.236.207L5.317 7.5l2.414.368c.18.03.269.108.269.232z" class="star"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_references.svg b/addons/website/static/src/img/snippets_thumbs/s_references.svg new file mode 100644 index 00000000..dbb24930 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_references.svg @@ -0,0 +1,111 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="10.577" height="8.581" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="46.271%" y2="33.176%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="38.751%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="9.615" height="8.581" x="0" y="0"/> + <linearGradient id="linearGradient-7" x1="72.875%" x2="40.332%" y1="45.488%" y2="29.644%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-8" x1="88.517%" x2="50%" y1="36.389%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-9" width="11.538" height="8.581" x="0" y="0"/> + <linearGradient id="linearGradient-11" x1="72.875%" x2="40.332%" y1="46.866%" y2="35.864%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-12" x1="88.517%" x2="50%" y1="40.548%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-13" width="10.577" height="8.581" x="0" y="0"/> + <rect id="path-15" width="22" height="2" x="17" y="0"/> + <filter id="filter-16" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-17" width="14" height="1" x="21" y="5"/> + <filter id="filter-18" width="107.1%" height="300%" x="-3.6%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_references"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(14 20)"> + <g class="image_1_border" transform="translate(30 11)"> + <rect width="11" height="9" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.212 .21)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="8.144" cy="2.198" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="1.587" ry="1.57"/> + <ellipse cx="10.683" cy="9.419" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="4.971" ry="3.14"/> + <ellipse cx=".106" cy="9.523" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="7.933" ry="4.919"/> + </g> + <path fill="#FFF" d="M11 0v9H0V0h11zm-1 1H1v7h9V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(45 11)"> + <rect width="10" height="9" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.192 .21)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-5"/> + <ellipse cx="7.404" cy="2.198" fill="#F3EC60" class="oval" mask="url(#mask-6)" rx="1.442" ry="1.57"/> + <ellipse cx="9.712" cy="9.419" fill="url(#linearGradient-7)" class="oval" mask="url(#mask-6)" rx="4.519" ry="3.14"/> + <ellipse cx=".096" cy="9.523" fill="url(#linearGradient-8)" class="oval" mask="url(#mask-6)" rx="7.212" ry="4.919"/> + </g> + <path fill="#FFF" d="M10 0v9H0V0h10zM9 1H1v7h8V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(0 11)"> + <rect width="12" height="9" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.23 .21)"> + <mask id="mask-10" fill="#fff"> + <use xlink:href="#path-9"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-9"/> + <ellipse cx="8.885" cy="2.198" fill="#F3EC60" class="oval" mask="url(#mask-10)" rx="1.731" ry="1.57"/> + <ellipse cx="11.654" cy="9.419" fill="url(#linearGradient-11)" class="oval" mask="url(#mask-10)" rx="5.423" ry="3.14"/> + <ellipse cx=".115" cy="9.523" fill="url(#linearGradient-12)" class="oval" mask="url(#mask-10)" rx="8.654" ry="4.919"/> + </g> + <path fill="#FFF" d="M12 0v9H0V0h12zm-1 1H1v7h10V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(15 11)"> + <rect width="11" height="9" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.212 .21)"> + <mask id="mask-14" fill="#fff"> + <use xlink:href="#path-13"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-13"/> + <ellipse cx="8.144" cy="2.198" fill="#F3EC60" class="oval" mask="url(#mask-14)" rx="1.587" ry="1.57"/> + <ellipse cx="10.683" cy="9.419" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-14)" rx="4.971" ry="3.14"/> + <ellipse cx=".106" cy="9.523" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-14)" rx="7.933" ry="4.919"/> + </g> + <path fill="#FFF" d="M11 0v9H0V0h11zm-1 1H1v7h9V1z" class="rectangle_2"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-16)" xlink:href="#path-15"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-15"/> + </g> + <g class="rectangle_copy"> + <use fill="#000" filter="url(#filter-18)" xlink:href="#path-17"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-17"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_share.svg b/addons/website/static/src/img/snippets_thumbs/s_share.svg new file mode 100644 index 00000000..a80bd232 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_share.svg @@ -0,0 +1,26 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M0 17.71V4.29C0 3.577.597 3 1.336 3h27.162l-1.91 1.843H1.909v12.313h32.184v-2.475L36 12.837v4.873c-.001.712-.598 1.29-1.336 1.29H1.336C.598 19 0 18.423 0 17.71z"/> + <filter id="filter-2" width="102.8%" height="112.5%" x="-1.4%" y="-3.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <linearGradient id="linearGradient-3" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_share"> + <rect width="82" height="60" class="bg"/> + <g class="noun_share_3132" transform="translate(21 21)"> + <g class="path"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <path fill="url(#linearGradient-3)" d="M33.852 2.409V0L40 6.234l-6.174 6.262V9.99s-3.621-.254-6.218 1.377C25.883 12.45 24.854 13.535 24 15c0 0 .233-3.89 2.44-7.433 2.76-4.43 7.412-5.158 7.412-5.158z" class="path"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_showcase.svg b/addons/website/static/src/img/snippets_thumbs/s_showcase.svg new file mode 100644 index 00000000..40dcdf57 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_showcase.svg @@ -0,0 +1,86 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="1" height="19" x="26" y="0"/> + <filter id="filter-2" width="200%" height="110.5%" x="-50%" y="-2.6%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-3" d="M13 16v1H2v-1h11zm0-11v1H5V5h8z"/> + <filter id="filter-4" width="109.1%" height="116.7%" x="-4.5%" y="-4.2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-5" d="M13 12v2H0v-2h13zm0-11v2H0V1h13z"/> + <filter id="filter-6" width="107.7%" height="115.4%" x="-3.8%" y="-3.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-7" width="13" height="5" x="20" y="22"/> + <filter id="filter-8" width="107.7%" height="140%" x="-3.8%" y="-10%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-9" width="11" height="3" x="21" y="23"/> + <filter id="filter-10" width="109.1%" height="166.7%" x="-4.5%" y="-16.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-11" d="M51 16v1H40v-1h11zm0-11v1H40V5h11z"/> + <filter id="filter-12" width="109.1%" height="116.7%" x="-4.5%" y="-4.2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-13" d="M54 12v2H40v-2h14zM50 1v2H40V1h10z"/> + <filter id="filter-14" width="107.1%" height="115.4%" x="-3.6%" y="-3.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <linearGradient id="linearGradient-15" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_showcase"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(14 17)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-5"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-7"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#000" fill-opacity=".348" xlink:href="#path-9"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-14)" xlink:href="#path-13"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-13"/> + </g> + <path fill="url(#linearGradient-15)" d="M18 12a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm0-11a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm17 11a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm0-11a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" class="combined_shape"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_table_of_content.svg b/addons/website/static/src/img/snippets_thumbs/s_table_of_content.svg new file mode 100644 index 00000000..944a6f22 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_table_of_content.svg @@ -0,0 +1,71 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M44 16v2H17v-2h27zm5-16v2H17V0h32z"/> + <filter id="filter-2" width="103.1%" height="111.1%" x="-1.6%" y="-2.8%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M29 24v1H17v-1h12zm13-3v1H17v-1h25zM40 8v1H17V8h23zm7-3v1H17V5h30z"/> + <filter id="filter-4" width="103.3%" height="110%" x="-1.7%" y="-2.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-5" width="1" height="29" x="13" y="0"/> + <filter id="filter-6" width="200%" height="106.9%" x="-50%" y="-1.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-7" width="10" height="1" x="0" y="0"/> + <filter id="filter-8" width="110%" height="300%" x="-5%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-9" width="10" height="1" x="0" y="3"/> + <filter id="filter-10" width="110%" height="300%" x="-5%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-11" width="10" height="1" x="0" y="6"/> + <filter id="filter-12" width="110%" height="300%" x="-5%" y="-50%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_table_of_content"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(17 16)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-9"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_tabs.svg b/addons/website/static/src/img/snippets_thumbs/s_tabs.svg new file mode 100644 index 00000000..0c221024 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_tabs.svg @@ -0,0 +1,93 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M39 23v1H20v-1h19zm6-3v1H20v-1h25z"/> + <filter id="filter-2" width="104%" height="150%" x="-2%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-3" width="15" height="2" x="20" y="15"/> + <filter id="filter-4" width="106.7%" height="200%" x="-3.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-5" width="12.5" height="10.488" x="0" y="0"/> + <linearGradient id="linearGradient-7" x1="72.875%" x2="40.332%" y1="46.011%" y2="32.006%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-8" x1="88.517%" x2="50%" y1="37.969%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <path id="path-9" d="M53 7v26H0V7h53zm-1 1H1v24h51V8z"/> + <filter id="filter-10" width="101.9%" height="107.7%" x="-.9%" y="-1.9%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-11" width="10" height="5" x="12" y="0"/> + <filter id="filter-12" width="110%" height="140%" x="-5%" y="-10%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-13" width="10" height="5" x="0" y="0"/> + <filter id="filter-14" width="110%" height="140%" x="-5%" y="-10%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-15" width="10" height="5" x="24" y="0"/> + <filter id="filter-16" width="110%" height="140%" x="-5%" y="-10%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_tabs"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 14)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-1"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-3"/> + </g> + <g class="image_1_border" transform="translate(4 14)"> + <rect width="13" height="11" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.25 .256)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-5"/> + <ellipse cx="9.625" cy="2.686" fill="#F3EC60" class="oval" mask="url(#mask-6)" rx="1.875" ry="1.919"/> + <ellipse cx="12.625" cy="11.512" fill="url(#linearGradient-7)" class="oval" mask="url(#mask-6)" rx="5.875" ry="3.837"/> + <ellipse cx=".125" cy="11.64" fill="url(#linearGradient-8)" class="oval" mask="url(#mask-6)" rx="9.375" ry="6.012"/> + </g> + <path fill="#FFF" d="M13 0v11H0V0h13zm-1 1H1v9h11V1z" class="rectangle_2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-9"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-14)" xlink:href="#path-13"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-13"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-16)" xlink:href="#path-15"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-15"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_text_block.svg b/addons/website/static/src/img/snippets_thumbs/s_text_block.svg new file mode 100644 index 00000000..a91d5d58 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_text_block.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M53 37v1H15v-1h38zm14-3v1H15v-1h52zm0-3v1H15v-1h52zm-24-5v1H15v-1h28zm24-3v1H15v-1h52z"/> + <filter id="filter-2" width="101.9%" height="113.3%" x="-1%" y="-3.3%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_text_block"> + <rect width="82" height="60" class="bg"/> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-1"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_text_highlight.svg b/addons/website/static/src/img/snippets_thumbs/s_text_highlight.svg new file mode 100644 index 00000000..77c432f5 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_text_highlight.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <linearGradient id="linearGradient-1" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <rect id="path-2" width="22" height="2" x="11" y="3"/> + <filter id="filter-3" width="104.5%" height="200%" x="-2.3%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.4 0"/> + </filter> + <path id="path-4" d="M28 11v1H16v-1h12zm2-3v1H14V8h16z"/> + <filter id="filter-5" width="106.2%" height="150%" x="-3.1%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.2 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_text_highlight"> + <rect width="82" height="60" class="bg"/> + <g class="group_2" transform="translate(19 22)"> + <rect width="44" height="17" fill="url(#linearGradient-1)" fill-opacity=".4" class="rectangle_2" rx="1"/> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-3)" xlink:href="#path-2"/> + <use fill="#FFF" fill-opacity=".95" xlink:href="#path-2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-5)" xlink:href="#path-4"/> + <use fill="#FFF" fill-opacity=".8" xlink:href="#path-4"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_text_image.svg b/addons/website/static/src/img/snippets_thumbs/s_text_image.svg new file mode 100644 index 00000000..7f335fc3 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_text_image.svg @@ -0,0 +1,53 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="23.077" height="18.116" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="46.509%" y2="34.249%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="39.469%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="25" height="2" x="0" y="0"/> + <filter id="filter-6" width="104%" height="200%" x="-2%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-7" d="M23 12v1H0v-1h23zm-4-3v1H0V9h19zm4-3v1H0V6h23z"/> + <filter id="filter-8" width="104.3%" height="128.6%" x="-2.2%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_text_image"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 21)"> + <g class="image_1_border" transform="translate(29)"> + <rect width="24" height="19" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.462 .442)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="17.769" cy="4.64" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="3.462" ry="3.314"/> + <ellipse cx="23.308" cy="19.884" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="10.846" ry="6.628"/> + <ellipse cx=".231" cy="20.105" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="17.308" ry="10.384"/> + </g> + <path fill="#FFF" d="M24 0v19H0V0h24zm-1 1H1v17h22V1z" class="rectangle_2"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_three_columns.svg b/addons/website/static/src/img/snippets_thumbs/s_three_columns.svg new file mode 100644 index 00000000..7b2fdafb --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_three_columns.svg @@ -0,0 +1,109 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="14.423" height="11.442" x="0" y="0"/> + <linearGradient id="linearGradient-3" x1="72.875%" x2="40.332%" y1="46.435%" y2="33.916%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-4" x1="88.517%" x2="50%" y1="39.246%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-5" width="15.385" height="11.442" x="0" y="0"/> + <linearGradient id="linearGradient-7" x1="72.875%" x2="40.332%" y1="46.866%" y2="35.864%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-8" x1="88.517%" x2="50%" y1="40.548%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <rect id="path-9" width="14.423" height="11.442" x="0" y="0"/> + <path id="path-11" d="M15 16v2H0v-2h15zm19 0v2H19v-2h15zm19 0v2H38v-2h15z"/> + <filter id="filter-12" width="101.9%" height="200%" x="-.9%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-13" d="M33 28v1H19v-1h14zm-3-3v1H19v-1h11zm3-3v1H19v-1h14z"/> + <filter id="filter-14" width="107.1%" height="128.6%" x="-3.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-15" d="M52 28v1H38v-1h14zm-3-3v1H38v-1h11zm3-3v1H38v-1h14z"/> + <filter id="filter-16" width="107.1%" height="128.6%" x="-3.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-17" d="M14 28v1H0v-1h14zm-3-3v1H0v-1h11zm3-3v1H0v-1h14z"/> + <filter id="filter-18" width="107.1%" height="128.6%" x="-3.6%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_three_columns"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(15 16)"> + <g class="image_1_border"> + <rect width="15" height="12" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.288 .28)"> + <mask id="mask-2" fill="#fff"> + <use xlink:href="#path-1"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-1"/> + <ellipse cx="11.106" cy="2.93" fill="#F3EC60" class="oval" mask="url(#mask-2)" rx="2.163" ry="2.093"/> + <ellipse cx="14.567" cy="12.558" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-2)" rx="6.779" ry="4.186"/> + <ellipse cx=".144" cy="12.698" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-2)" rx="10.817" ry="6.558"/> + </g> + <path fill="#FFF" d="M15 0v12H0V0h15zm-1 1H1v10h13V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(18)"> + <rect width="16" height="12" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.308 .28)"> + <mask id="mask-6" fill="#fff"> + <use xlink:href="#path-5"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-5"/> + <ellipse cx="11.846" cy="2.93" fill="#F3EC60" class="oval" mask="url(#mask-6)" rx="2.308" ry="2.093"/> + <ellipse cx="15.538" cy="12.558" fill="url(#linearGradient-7)" class="oval" mask="url(#mask-6)" rx="7.231" ry="4.186"/> + <ellipse cx=".154" cy="12.698" fill="url(#linearGradient-8)" class="oval" mask="url(#mask-6)" rx="11.538" ry="6.558"/> + </g> + <path fill="#FFF" d="M16 0v12H0V0h16zm-1 1H1v10h14V1z" class="rectangle_2"/> + </g> + <g class="image_1_border" transform="translate(38)"> + <rect width="15" height="12" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.288 .28)"> + <mask id="mask-10" fill="#fff"> + <use xlink:href="#path-9"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-9"/> + <ellipse cx="11.106" cy="2.93" fill="#F3EC60" class="oval" mask="url(#mask-10)" rx="2.163" ry="2.093"/> + <ellipse cx="14.567" cy="12.558" fill="url(#linearGradient-3)" class="oval" mask="url(#mask-10)" rx="6.779" ry="4.186"/> + <ellipse cx=".144" cy="12.698" fill="url(#linearGradient-4)" class="oval" mask="url(#mask-10)" rx="10.817" ry="6.558"/> + </g> + <path fill="#FFF" d="M15 0v12H0V0h15zm-1 1H1v10h13V1z" class="rectangle_2"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-11"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-14)" xlink:href="#path-13"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-13"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-16)" xlink:href="#path-15"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-15"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-18)" xlink:href="#path-17"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-17"/> + </g> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_timeline.svg b/addons/website/static/src/img/snippets_thumbs/s_timeline.svg new file mode 100644 index 00000000..d66bf71e --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_timeline.svg @@ -0,0 +1,88 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <rect id="path-1" width="10" height="2" x="4" y="25"/> + <filter id="filter-2" width="110%" height="200%" x="-5%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-3" width="13" height="2" x="1" y="1"/> + <filter id="filter-4" width="107.7%" height="200%" x="-3.8%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-5" d="M14 33v1H3v-1h11zm0-3v1H0v-1h14z"/> + <filter id="filter-6" width="107.1%" height="150%" x="-3.6%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-7" d="M37 21v1h-7v-1h7zm7-3v1H30v-1h14z"/> + <filter id="filter-8" width="107.1%" height="150%" x="-3.6%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-9" width="16" height="2" x="30" y="13"/> + <filter id="filter-10" width="106.2%" height="200%" x="-3.1%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-11" width="1" height="34" x="22" y="0"/> + <filter id="filter-12" width="200%" height="105.9%" x="-50%" y="-1.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <path id="path-13" d="M14 9v1H4V9h10zm0-3v1H4V6h10z"/> + <filter id="filter-14" width="110%" height="150%" x="-5%" y="-12.5%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <linearGradient id="linearGradient-15" x1="0%" x2="100%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_timeline"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(18 13)"> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-3"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-5"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-8)" xlink:href="#path-7"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-7"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-10)" xlink:href="#path-9"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-9"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-12)" xlink:href="#path-11"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-11"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-14)" xlink:href="#path-13"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-13"/> + </g> + <circle cx="22.5" cy="2.5" r="2.5" fill="url(#linearGradient-15)" class="oval"/> + <circle cx="22.5" cy="14.5" r="2.5" fill="url(#linearGradient-15)" class="oval"/> + <circle cx="22.5" cy="26.5" r="2.5" fill="url(#linearGradient-15)" class="oval"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_title.svg b/addons/website/static/src/img/snippets_thumbs/s_title.svg new file mode 100644 index 00000000..ca12e48d --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_title.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="82" height="60" viewBox="0 0 82 60"> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_title"> + <rect width="82" height="60" class="bg"/> + <path fill="#E4E4E4" fill-rule="nonzero" d="M19.076 33.364v-.381c-.415-.015-.71-.066-.887-.154-.302-.156-.454-.456-.454-.9V28.47h3.575v3.457c0 .43-.14.723-.418.88-.18.102-.486.16-.915.175v.38h5.105v-.38c-.4-.015-.686-.068-.857-.161-.298-.156-.447-.454-.447-.894V24.67c0-.43.134-.72.403-.872.16-.092.461-.156.9-.19v-.38h-5.104v.38c.454.03.764.09.93.183.268.151.403.444.403.879v3.105h-3.575V24.67c0-.435.142-.73.425-.886.171-.093.476-.152.916-.176v-.38H14v.38c.444.044.742.11.894.198.249.151.373.44.373.864v7.258c0 .435-.14.73-.417.886-.166.093-.45.15-.85.169v.38h5.076zm9.697.212c.513 0 .967-.095 1.362-.285.61-.298 1.148-.828 1.612-1.59l-.345-.212c-.283.312-.52.534-.71.666-.313.22-.645.33-.996.33-.738 0-1.253-.413-1.546-1.238-.16-.44-.258-.96-.293-1.56h3.824c0-.137-.017-.334-.052-.593-.068-.552-.19-1.001-.366-1.348a2.605 2.605 0 0 0-1.003-1.077 2.675 2.675 0 0 0-1.392-.388c-.864 0-1.603.319-2.215.956-.613.637-.92 1.532-.92 2.684 0 1.275.323 2.203.967 2.784.645.58 1.336.871 2.073.871zm1.062-4.446h-2.014c.02-.776.101-1.367.245-1.772s.412-.608.802-.608c.381 0 .635.176.762.527.127.352.195.97.205 1.853zm7.859 4.446c.249 0 .486-.044.71-.132.357-.136.681-.376.974-.717l-.227-.315a.981.981 0 0 1-.216.18.363.363 0 0 1-.157.032c-.069 0-.13-.038-.187-.114a.448.448 0 0 1-.084-.274v-3.633c0-.889-.283-1.499-.85-1.831-.571-.327-1.282-.49-2.131-.49-.791 0-1.458.168-2 .505-.542.337-.813.793-.813 1.37 0 .322.102.568.304.739.203.17.448.256.736.256.25 0 .468-.077.656-.23.188-.154.282-.373.282-.656a.824.824 0 0 0-.062-.318 1.08 1.08 0 0 0-.165-.275l-.088-.103a.588.588 0 0 1-.088-.124.326.326 0 0 1-.03-.147c0-.151.087-.277.26-.377.174-.1.395-.15.664-.15.478 0 .811.106 1 .319.187.212.281.54.281.985v1.091c-1.386.4-2.404.798-3.054 1.194-.65.395-.974.93-.974 1.604 0 .552.178.958.535 1.22.356.26.74.391 1.15.391.307 0 .62-.054.937-.161.523-.17.991-.464 1.406-.879.054.288.14.506.257.652.205.259.53.388.974.388zm-2.351-1.07c-.186 0-.353-.083-.502-.252-.149-.168-.223-.416-.223-.743 0-.552.256-1.006.769-1.363.302-.21.654-.363 1.054-.461v2.19c-.16.186-.3.322-.417.41-.21.147-.437.22-.681.22zm9.294 1.07c.615-.17 1.045-.277 1.29-.318.243-.042.785-.104 1.625-.187v-.344c-.342-.03-.571-.104-.688-.224-.117-.12-.176-.336-.176-.648v-8.628h-3.23v.366c.459.02.762.075.908.165.147.09.22.324.22.7v2.746c-.288-.303-.527-.513-.718-.63a1.925 1.925 0 0 0-1.055-.293c-.79 0-1.468.343-2.032 1.03-.564.685-.846 1.622-.846 2.808 0 1.065.271 1.907.813 2.527.542.62 1.18.93 1.912.93.43 0 .83-.112 1.2-.337.235-.141.494-.359.777-.652v.99zm-1.304-.85c-.512 0-.859-.353-1.04-1.061-.097-.391-.146-.972-.146-1.744 0-.722.051-1.281.154-1.677.19-.737.552-1.106 1.084-1.106.361 0 .652.12.871.36.22.238.33.419.33.541v3.648c0 .117-.128.32-.385.607-.256.289-.545.433-.868.433zm6.629-7.397c.317 0 .59-.113.817-.34.227-.228.34-.502.34-.824a1.12 1.12 0 0 0-.34-.824 1.116 1.116 0 0 0-.817-.341c-.322 0-.597.114-.824.34-.227.228-.34.502-.34.825 0 .322.113.596.34.824.227.227.502.34.824.34zm1.75 8.035v-.36c-.268-.053-.451-.126-.549-.219-.098-.093-.146-.303-.146-.63v-5.698h-2.879v.366c.303.054.506.136.608.246.103.11.154.316.154.619v4.409c0 .341-.073.578-.22.71-.097.088-.278.154-.542.198v.359h3.574zm4.205 0v-.36c-.27-.053-.452-.126-.55-.219-.097-.093-.146-.303-.146-.63v-4.013c.117-.19.284-.38.501-.568.218-.188.456-.282.715-.282.346 0 .578.154.695.461.069.171.103.428.103.77v3.632c0 .327-.049.537-.147.63-.097.093-.28.166-.549.22v.359h3.523v-.36c-.269-.033-.46-.1-.575-.197-.115-.098-.172-.315-.172-.652V28.53c0-.835-.177-1.419-.531-1.75-.354-.333-.853-.499-1.498-.499-.45 0-.858.119-1.227.355a3.073 3.073 0 0 0-.912.898v-1.077h-2.841v.366c.322.04.536.117.64.235.106.117.158.327.158.63v4.409c0 .341-.065.57-.194.684-.13.115-.33.19-.604.224v.359h3.61zm7.91 3.113c.859 0 1.572-.096 2.138-.286 1.075-.366 1.612-1.038 1.612-2.014 0-.757-.36-1.282-1.077-1.575-.376-.151-.84-.232-1.392-.242l-.974-.014c-.132 0-.317-.004-.556-.011a5.941 5.941 0 0 1-.47-.026.579.579 0 0 1-.343-.154.475.475 0 0 1-.125-.351c0-.19.083-.354.25-.491a1.01 1.01 0 0 1 .46-.234c.093 0 .163.002.21.007.046.005.145.007.296.007.698 0 1.282-.093 1.75-.278.894-.347 1.34-.989 1.34-1.926 0-.298-.052-.574-.157-.828a1.975 1.975 0 0 0-.45-.666h1.216v-.799h-1.978a3.807 3.807 0 0 0-.681-.212 4.256 4.256 0 0 0-.967-.103c-.932 0-1.672.222-2.219.667-.547.444-.82 1.008-.82 1.692 0 .542.156 1.003.468 1.384.313.38.723.674 1.23.879v.102c-.35.118-.71.325-1.08.623-.368.298-.552.657-.552 1.077 0 .351.117.632.351.842.132.117.344.232.637.344v.103c-.44.068-.748.216-.926.443-.178.227-.268.448-.268.663 0 .551.42.942 1.26 1.171.508.137 1.114.206 1.817.206zm.102-5.838c-.415 0-.696-.22-.842-.659-.098-.278-.147-.706-.147-1.282 0-.63.066-1.11.198-1.439.132-.33.396-.494.791-.494.362 0 .617.153.766.461.149.308.223.798.223 1.472 0 .635-.07 1.117-.212 1.447-.142.33-.4.494-.777.494zm.066 5.347c-.625 0-1.102-.088-1.432-.264-.33-.176-.494-.417-.494-.725 0-.18.06-.351.183-.513.068-.088.18-.195.337-.322h2.46c.509 0 .859.065 1.052.194.193.13.29.324.29.582 0 .445-.34.75-1.019.916-.361.088-.82.132-1.377.132z" class="heading"/> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_twitter_scroll.svg b/addons/website/static/src/img/snippets_thumbs/s_twitter_scroll.svg new file mode 100644 index 00000000..6cf17fa1 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_twitter_scroll.svg @@ -0,0 +1,74 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M0 1v27a1 1 0 0 0 1 1h40V1a1 1 0 0 0-1-1H1a1 1 0 0 0-1 1zm1 27.188V1h39v13.797l1 1.217V29H23l-1.5-.812H1z"/> + <filter id="filter-2" width="102.4%" height="106.9%" x="-1.2%" y="-1.7%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M17 11v1h-4v-1h4zm6-3v1H13V8h10zm7-3v1H13V5h17z"/> + <filter id="filter-4" width="105.9%" height="128.6%" x="-2.9%" y="-7.1%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <rect id="path-5" width="16" height="2" x="13" y="0"/> + <filter id="filter-6" width="106.2%" height="200%" x="-3.1%" y="-25%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <rect id="path-7" width="10.353" height="13.349" x="0" y="0"/> + <linearGradient id="linearGradient-9" x1="72.875%" x2="40.332%" y1="46.142%" y2="32.596%"> + <stop offset="0%" stop-color="#008374"/> + <stop offset="100%" stop-color="#006A59"/> + </linearGradient> + <linearGradient id="linearGradient-10" x1="88.517%" x2="50%" y1="38.363%" y2="50%"> + <stop offset="0%" stop-color="#00AA89"/> + <stop offset="100%" stop-color="#009989"/> + </linearGradient> + <linearGradient id="linearGradient-11" x1="50%" x2="50%" y1="0%" y2="100%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + <path id="path-12" d="M44 20.781a7.843 7.843 0 0 1-1.85 1.957 11.013 11.013 0 0 1-.423 3.533 11.124 11.124 0 0 1-1.319 2.913c-.59.93-1.293 1.752-2.107 2.466-.815.715-1.797 1.286-2.947 1.711-1.15.426-2.38.639-3.689.639-2.063 0-3.952-.566-5.665-1.7.266.032.563.048.89.048 1.714 0 3.24-.54 4.58-1.618a3.539 3.539 0 0 1-2.146-.755 3.701 3.701 0 0 1-1.302-1.87c.25.04.483.06.696.06.328 0 .651-.044.971-.13a3.614 3.614 0 0 1-2.119-1.306c-.56-.692-.839-1.495-.839-2.409v-.047a3.59 3.59 0 0 0 1.667.48 3.747 3.747 0 0 1-1.199-1.347 3.764 3.764 0 0 1-.445-1.804c0-.688.167-1.325.502-1.91a10.515 10.515 0 0 0 3.364 2.794c1.321.7 2.735 1.088 4.243 1.166a4.31 4.31 0 0 1-.091-.867c0-1.047.36-1.94 1.079-2.678.72-.738 1.59-1.107 2.61-1.107 1.066 0 1.964.398 2.695 1.195a7.123 7.123 0 0 0 2.341-.914 3.66 3.66 0 0 1-1.621 2.086A7.204 7.204 0 0 0 44 20.781z"/> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_twitter_scroll"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(19 13)"> + <g class="shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="group_2" transform="translate(5 6)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <g class="rectangle"> + <use fill="#000" filter="url(#filter-6)" xlink:href="#path-5"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-5"/> + </g> + <g class="image_1_border"> + <rect width="11" height="14" fill="#FFF" class="rectangle"/> + <g class="oval___oval_mask" transform="translate(.324 .326)"> + <mask id="mask-8" fill="#fff"> + <use xlink:href="#path-7"/> + </mask> + <use fill="#79D1F2" class="mask" xlink:href="#path-7"/> + <ellipse cx="6.632" cy="3.419" fill="#F3EC60" class="oval" mask="url(#mask-8)" rx="2.426" ry="2.442"/> + <ellipse cx="10.515" cy="14.651" fill="url(#linearGradient-9)" class="oval" mask="url(#mask-8)" rx="7.603" ry="4.884"/> + <ellipse cx="-5.662" cy="14.814" fill="url(#linearGradient-10)" class="oval" mask="url(#mask-8)" rx="12.132" ry="7.651"/> + </g> + <path fill="#FFF" d="M11 0v14H0V0h11zm-1 1H1v12h9V1z" class="rectangle_2"/> + </g> + </g> + <mask id="mask-13" fill="#fff"> + <use xlink:href="#path-12"/> + </mask> + <use fill="url(#linearGradient-11)" class="twitter" xlink:href="#path-12"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/snippets_thumbs/s_website_form.svg b/addons/website/static/src/img/snippets_thumbs/s_website_form.svg new file mode 100644 index 00000000..ab3bda26 --- /dev/null +++ b/addons/website/static/src/img/snippets_thumbs/s_website_form.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="82" height="60" viewBox="0 0 82 60"> + <defs> + <path id="path-1" d="M19 21v4h-5v-4h5zm7 0v4h-5v-4h5zm-8 1h-3v2h3v-2zm7 0h-3v2h3v-2zm23-8v4H14v-4h34zm-1 1H15v2h32v-2zm1-8v4H14V7h34zm-1 1H15v2h32V8zm1-8v4H14V0h34zm-1 1H15v2h32V1z"/> + <filter id="filter-2" width="102.9%" height="108%" x="-1.5%" y="-2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.292012675 0"/> + </filter> + <path id="path-3" d="M10 21v2H2v-2h8zm0-7v2H2v-2h8zm0-7v2H3V7h7zm0-7v2H0V0h10z"/> + <filter id="filter-4" width="110%" height="108.7%" x="-5%" y="-2.2%" filterUnits="objectBoundingBox"> + <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/> + <feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/> + <feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.0995137675 0"/> + </filter> + <linearGradient id="linearGradient-5" x1="0%" x2="100%" y1="45.675%" y2="54.325%"> + <stop offset="0%" stop-color="#00A09D"/> + <stop offset="100%" stop-color="#00E2FF"/> + </linearGradient> + </defs> + <g fill="none" fill-rule="evenodd" class="snippets_thumbs"> + <g class="s_website_form"> + <rect width="82" height="60" class="bg"/> + <g class="group" transform="translate(17 13)"> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-2)" xlink:href="#path-1"/> + <use fill="#FFF" fill-opacity=".78" xlink:href="#path-1"/> + </g> + <g class="combined_shape"> + <use fill="#000" filter="url(#filter-4)" xlink:href="#path-3"/> + <use fill="#FFF" fill-opacity=".348" xlink:href="#path-3"/> + </g> + <rect width="17" height="5" x="14" y="30" fill="url(#linearGradient-5)" class="rectangle"/> + </g> + </g> + </g> +</svg> diff --git a/addons/website/static/src/img/theme_loader.gif b/addons/website/static/src/img/theme_loader.gif Binary files differnew file mode 100644 index 00000000..a5c32657 --- /dev/null +++ b/addons/website/static/src/img/theme_loader.gif diff --git a/addons/website/static/src/img/website-visitors.gif b/addons/website/static/src/img/website-visitors.gif Binary files differnew file mode 100644 index 00000000..5d4aed82 --- /dev/null +++ b/addons/website/static/src/img/website-visitors.gif diff --git a/addons/website/static/src/img/website_dashboard_visit_demo.png b/addons/website/static/src/img/website_dashboard_visit_demo.png Binary files differnew file mode 100644 index 00000000..7945ac17 --- /dev/null +++ b/addons/website/static/src/img/website_dashboard_visit_demo.png diff --git a/addons/website/static/src/img/website_logo.png b/addons/website/static/src/img/website_logo.png Binary files differnew file mode 100644 index 00000000..16f69ed2 --- /dev/null +++ b/addons/website/static/src/img/website_logo.png diff --git a/addons/website/static/src/js/backend/button.js b/addons/website/static/src/js/backend/button.js new file mode 100644 index 00000000..bb3c5bd1 --- /dev/null +++ b/addons/website/static/src/js/backend/button.js @@ -0,0 +1,116 @@ +odoo.define('website.backend.button', function (require) { +'use strict'; + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); +var field_registry = require('web.field_registry'); + +var _t = core._t; + +var WebsitePublishButton = AbstractField.extend({ + className: 'o_stat_info', + supportedFieldTypes: ['boolean'], + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * A boolean field is always set since false is a valid value. + * + * @override + */ + isSet: function () { + return true; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * This widget is supposed to be used inside a stat button and, as such, is + * rendered the same way in edit and readonly mode. + * + * @override + * @private + */ + _render: function () { + this.$el.empty(); + var text = this.value ? _t("Published") : _t("Unpublished"); + var hover = this.value ? _t("Unpublish") : _t("Publish"); + var valColor = this.value ? 'text-success' : 'text-danger'; + var hoverColor = this.value ? 'text-danger' : 'text-success'; + var $val = $('<span>').addClass('o_stat_text o_not_hover ' + valColor).text(text); + var $hover = $('<span>').addClass('o_stat_text o_hover ' + hoverColor).text(hover); + this.$el.append($val).append($hover); + }, +}); + +var WidgetWebsiteButtonIcon = AbstractField.extend({ + template: 'WidgetWebsiteButtonIcon', + events: { + 'click': '_onClick', + }, + + /** + * @override + */ + start: function () { + this.$icon = this.$('.o_button_icon'); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + isSet: function () { + return true; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _render: function () { + this._super.apply(this, arguments); + + var published = this.value; + var info = published ? _t("Published") : _t("Unpublished"); + this.$el.attr('aria-label', info) + .prop('title', info); + this.$icon.toggleClass('text-danger', !published) + .toggleClass('text-success', published); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * Redirects to the website page of the record. + * + * @private + */ + _onClick: function () { + this.trigger_up('button_clicked', { + attrs: { + type: 'object', + name: 'open_website_url', + }, + record: this.record, + }); + }, +}); + +field_registry + .add('website_redirect_button', WidgetWebsiteButtonIcon) + .add('website_publish_button', WebsitePublishButton); +}); diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js new file mode 100644 index 00000000..b7050675 --- /dev/null +++ b/addons/website/static/src/js/backend/dashboard.js @@ -0,0 +1,721 @@ +odoo.define('website.backend.dashboard', function (require) { +'use strict'; + +var AbstractAction = require('web.AbstractAction'); +var ajax = require('web.ajax'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var field_utils = require('web.field_utils'); +var pyUtils = require('web.py_utils'); +var session = require('web.session'); +var time = require('web.time'); +var web_client = require('web.web_client'); + +var _t = core._t; +var QWeb = core.qweb; + +var COLORS = ["#1f77b4", "#aec7e8"]; +var FORMAT_OPTIONS = { + // allow to decide if utils.human_number should be used + humanReadable: function (value) { + return Math.abs(value) >= 1000; + }, + // with the choices below, 1236 is represented by 1.24k + minDigits: 1, + decimals: 2, + // avoid comma separators for thousands in numbers when human_number is used + formatterCallback: function (str) { + return str; + }, +}; + +var Dashboard = AbstractAction.extend({ + hasControlPanel: true, + contentTemplate: 'website.WebsiteDashboardMain', + jsLibs: [ + '/web/static/lib/Chart/Chart.js', + ], + events: { + 'click .js_link_analytics_settings': 'on_link_analytics_settings', + 'click .o_dashboard_action': 'on_dashboard_action', + 'click .o_dashboard_action_form': 'on_dashboard_action_form', + }, + + init: function(parent, context) { + this._super(parent, context); + + this.DATE_FORMAT = time.getLangDateFormat(); + this.date_range = 'week'; // possible values : 'week', 'month', year' + this.date_from = moment.utc().subtract(1, 'week'); + this.date_to = moment.utc(); + + this.dashboards_templates = ['website.dashboard_header', 'website.dashboard_content']; + this.graphs = []; + this.chartIds = {}; + }, + + willStart: function() { + var self = this; + return Promise.all([ajax.loadLibs(this), this._super()]).then(function() { + return self.fetch_data(); + }).then(function(){ + var website = _.findWhere(self.websites, {selected: true}); + self.website_id = website ? website.id : false; + }); + }, + + start: function() { + var self = this; + this._computeControlPanelProps(); + return this._super().then(function() { + self.render_graphs(); + }); + }, + + on_attach_callback: function () { + this._isInDom = true; + this.render_graphs(); + this._super.apply(this, arguments); + }, + on_detach_callback: function () { + this._isInDom = false; + this._super.apply(this, arguments); + }, + /** + * Fetches dashboard data + */ + fetch_data: function() { + var self = this; + var prom = this._rpc({ + route: '/website/fetch_dashboard_data', + params: { + website_id: this.website_id || false, + date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(), + date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(), + }, + }); + prom.then(function (result) { + self.data = result; + self.dashboards_data = result.dashboards; + self.currency_id = result.currency_id; + self.groups = result.groups; + self.websites = result.websites; + }); + return prom; + }, + + on_link_analytics_settings: function(ev) { + ev.preventDefault(); + + var self = this; + var dialog = new Dialog(this, { + size: 'medium', + title: _t('Connect Google Analytics'), + $content: QWeb.render('website.ga_dialog_content', { + ga_key: this.dashboards_data.visits.ga_client_id, + ga_analytics_key: this.dashboards_data.visits.ga_analytics_key, + }), + buttons: [ + { + text: _t("Save"), + classes: 'btn-primary', + close: true, + click: function() { + var ga_client_id = dialog.$el.find('input[name="ga_client_id"]').val(); + var ga_analytics_key = dialog.$el.find('input[name="ga_analytics_key"]').val(); + self.on_save_ga_client_id(ga_client_id, ga_analytics_key); + }, + }, + { + text: _t("Cancel"), + close: true, + }, + ], + }).open(); + }, + + on_go_to_website: function (ev) { + ev.preventDefault(); + var website = _.findWhere(this.websites, {selected: true}); + window.location.href = `/website/force/${website.id}`; + }, + + on_save_ga_client_id: function(ga_client_id, ga_analytics_key) { + var self = this; + return this._rpc({ + route: '/website/dashboard/set_ga_data', + params: { + 'website_id': self.website_id, + 'ga_client_id': ga_client_id, + 'ga_analytics_key': ga_analytics_key, + }, + }).then(function (result) { + if (result.error) { + self.do_warn(result.error.title, result.error.message); + return; + } + self.on_date_range_button('week'); + }); + }, + + render_dashboards: function() { + var self = this; + _.each(this.dashboards_templates, function(template) { + self.$('.o_website_dashboard').append(QWeb.render(template, {widget: self})); + }); + }, + + render_graph: function(div_to_display, chart_values, chart_id) { + var self = this; + + this.$(div_to_display).empty(); + var $canvasContainer = $('<div/>', {class: 'o_graph_canvas_container'}); + this.$canvas = $('<canvas/>').attr('id', chart_id); + $canvasContainer.append(this.$canvas); + this.$(div_to_display).append($canvasContainer); + + var labels = chart_values[0].values.map(function (date) { + return moment(date[0], "YYYY-MM-DD", 'en'); + }); + + var datasets = chart_values.map(function (group, index) { + return { + label: group.key, + data: group.values.map(function (value) { + return value[1]; + }), + dates: group.values.map(function (value) { + return value[0]; + }), + fill: false, + borderColor: COLORS[index], + }; + }); + + var ctx = this.$canvas[0]; + this.chart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: datasets, + }, + options: { + legend: { + display: false, + }, + maintainAspectRatio: false, + scales: { + yAxes: [{ + type: 'linear', + ticks: { + beginAtZero: true, + callback: this.formatValue.bind(this), + }, + }], + xAxes: [{ + ticks: { + callback: function (moment) { + return moment.format(self.DATE_FORMAT); + }, + } + }], + }, + tooltips: { + mode: 'index', + intersect: false, + bodyFontColor: 'rgba(0,0,0,1)', + titleFontSize: 13, + titleFontColor: 'rgba(0,0,0,1)', + backgroundColor: 'rgba(255,255,255,0.6)', + borderColor: 'rgba(0,0,0,0.2)', + borderWidth: 2, + callbacks: { + title: function (tooltipItems, data) { + return data.datasets[0].label; + }, + label: function (tooltipItem, data) { + var moment = data.labels[tooltipItem.index]; + var date = tooltipItem.datasetIndex === 0 ? + moment : + moment.subtract(1, self.date_range); + return date.format(self.DATE_FORMAT) + ': ' + self.formatValue(tooltipItem.yLabel); + }, + labelColor: function (tooltipItem, chart) { + var dataset = chart.data.datasets[tooltipItem.datasetIndex]; + return { + borderColor: dataset.borderColor, + backgroundColor: dataset.borderColor, + }; + }, + } + } + } + }); + }, + + render_graphs: function() { + var self = this; + if (this._isInDom) { + _.each(this.graphs, function(e) { + var renderGraph = self.groups[e.group] && + self.dashboards_data[e.name].summary.order_count; + if (!self.chartIds[e.name]) { + self.chartIds[e.name] = _.uniqueId('chart_' + e.name); + } + var chart_id = self.chartIds[e.name]; + if (renderGraph) { + self.render_graph('.o_graph_' + e.name, self.dashboards_data[e.name].graph, chart_id); + } + }); + this.render_graph_analytics(this.dashboards_data.visits.ga_client_id); + } + }, + + render_graph_analytics: function(client_id) { + if (!this.dashboards_data.visits || !this.dashboards_data.visits.ga_client_id) { + return; + } + + this.load_analytics_api(); + + var $analytics_components = this.$('.js_analytics_components'); + this.addLoader($analytics_components); + + var self = this; + gapi.analytics.ready(function() { + + $analytics_components.empty(); + // 1. Authorize component + var $analytics_auth = $('<div>').addClass('col-lg-12'); + window.onOriginError = function () { + $analytics_components.find('.js_unauthorized_message').remove(); + self.display_unauthorized_message($analytics_components, 'not_initialized'); + }; + gapi.analytics.auth.authorize({ + container: $analytics_auth[0], + clientid: client_id + }); + + $analytics_auth.appendTo($analytics_components); + + self.handle_analytics_auth($analytics_components); + gapi.analytics.auth.on('signIn', function() { + delete window.onOriginError; + self.handle_analytics_auth($analytics_components); + }); + + }); + }, + + on_date_range_button: function(date_range) { + if (date_range === 'week') { + this.date_range = 'week'; + this.date_from = moment.utc().subtract(1, 'weeks'); + } else if (date_range === 'month') { + this.date_range = 'month'; + this.date_from = moment.utc().subtract(1, 'months'); + } else if (date_range === 'year') { + this.date_range = 'year'; + this.date_from = moment.utc().subtract(1, 'years'); + } else { + console.log('Unknown date range. Choose between [week, month, year]'); + return; + } + + var self = this; + Promise.resolve(this.fetch_data()).then(function () { + self.$('.o_website_dashboard').empty(); + self.render_dashboards(); + self.render_graphs(); + }); + + }, + + on_website_button: function(website_id) { + var self = this; + this.website_id = website_id; + Promise.resolve(this.fetch_data()).then(function () { + self.$('.o_website_dashboard').empty(); + self.render_dashboards(); + self.render_graphs(); + }); + }, + + on_reverse_breadcrumb: function() { + var self = this; + web_client.do_push_state({}); + this.fetch_data().then(function() { + self.$('.o_website_dashboard').empty(); + self.render_dashboards(); + self.render_graphs(); + }); + }, + + on_dashboard_action: function (ev) { + ev.preventDefault(); + var self = this + var $action = $(ev.currentTarget); + var additional_context = {}; + if (this.date_range === 'week') { + additional_context = {search_default_week: true}; + } else if (this.date_range === 'month') { + additional_context = {search_default_month: true}; + } else if (this.date_range === 'year') { + additional_context = {search_default_year: true}; + } + this._rpc({ + route: '/web/action/load', + params: { + 'action_id': $action.attr('name'), + }, + }) + .then(function (action) { + action.domain = pyUtils.assembleDomains([action.domain, `[('website_id', '=', ${self.website_id})]`]); + return self.do_action(action, { + 'additional_context': additional_context, + 'on_reverse_breadcrumb': self.on_reverse_breadcrumb + }); + }); + }, + + on_dashboard_action_form: function (ev) { + ev.preventDefault(); + var $action = $(ev.currentTarget); + this.do_action({ + name: $action.attr('name'), + res_model: $action.data('res_model'), + res_id: $action.data('res_id'), + views: [[false, 'form']], + type: 'ir.actions.act_window', + }, { + on_reverse_breadcrumb: this.on_reverse_breadcrumb + }); + }, + + /** + * @private + */ + _computeControlPanelProps() { + const $searchview = $(QWeb.render("website.DateRangeButtons", { + widget: this, + })); + $searchview.find('button.js_date_range').click((ev) => { + $searchview.find('button.js_date_range.active').removeClass('active'); + $(ev.target).addClass('active'); + this.on_date_range_button($(ev.target).data('date')); + }); + $searchview.find('button.js_website').click((ev) => { + $searchview.find('button.js_website.active').removeClass('active'); + $(ev.target).addClass('active'); + this.on_website_button($(ev.target).data('website-id')); + }); + + const $buttons = $(QWeb.render("website.GoToButtons")); + $buttons.on('click', this.on_go_to_website.bind(this)); + + this.controlPanelProps.cp_content = { $searchview, $buttons }; + }, + + // Loads Analytics API + load_analytics_api: function() { + var self = this; + if (!("gapi" in window)) { + (function(w,d,s,g,js,fjs){ + g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(cb){this.q.push(cb);}}; + js=d.createElement(s);fjs=d.getElementsByTagName(s)[0]; + js.src='https://apis.google.com/js/platform.js'; + fjs.parentNode.insertBefore(js,fjs);js.onload=function(){g.load('analytics');}; + }(window,document,'script')); + gapi.analytics.ready(function() { + self.analytics_create_components(); + }); + } + }, + + handle_analytics_auth: function($analytics_components) { + $analytics_components.find('.js_unauthorized_message').remove(); + + // Check if the user is authenticated and has the right to make API calls + if (!gapi.analytics.auth.getAuthResponse()) { + this.display_unauthorized_message($analytics_components, 'not_connected'); + } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) { + this.display_unauthorized_message($analytics_components, 'no_right'); + } else { + this.make_analytics_calls($analytics_components); + } + }, + + display_unauthorized_message: function($analytics_components, reason) { + $analytics_components.prepend($(QWeb.render('website.unauthorized_analytics', {reason: reason}))); + }, + + make_analytics_calls: function($analytics_components) { + // 2. ActiveUsers component + var $analytics_users = $('<div>'); + var activeUsers = new gapi.analytics.ext.ActiveUsers({ + container: $analytics_users[0], + pollingInterval: 10, + }); + $analytics_users.appendTo($analytics_components); + + // 3. View Selector + var $analytics_view_selector = $('<div>').addClass('col-lg-12 o_properties_selection'); + var viewSelector = new gapi.analytics.ViewSelector({ + container: $analytics_view_selector[0], + }); + viewSelector.execute(); + $analytics_view_selector.appendTo($analytics_components); + + // 4. Chart graph + var start_date = '7daysAgo'; + if (this.date_range === 'month') { + start_date = '30daysAgo'; + } else if (this.date_range === 'year') { + start_date = '365daysAgo'; + } + var $analytics_chart_2 = $('<div>').addClass('col-lg-6 col-12'); + var breakdownChart = new gapi.analytics.googleCharts.DataChart({ + query: { + 'dimensions': 'ga:date', + 'metrics': 'ga:sessions', + 'start-date': start_date, + 'end-date': 'yesterday' + }, + chart: { + type: 'LINE', + container: $analytics_chart_2[0], + options: { + title: 'All', + width: '100%', + tooltip: {isHtml: true}, + } + } + }); + $analytics_chart_2.appendTo($analytics_components); + + // 5. Chart table + var $analytics_chart_1 = $('<div>').addClass('col-lg-6 col-12'); + var mainChart = new gapi.analytics.googleCharts.DataChart({ + query: { + 'dimensions': 'ga:medium', + 'metrics': 'ga:sessions', + 'sort': '-ga:sessions', + 'max-results': '6' + }, + chart: { + type: 'TABLE', + container: $analytics_chart_1[0], + options: { + width: '100%' + } + } + }); + $analytics_chart_1.appendTo($analytics_components); + + // Events handling & animations + + var table_row_listener; + + viewSelector.on('change', function(ids) { + var options = {query: {ids: ids}}; + activeUsers.set({ids: ids}).execute(); + mainChart.set(options).execute(); + breakdownChart.set(options).execute(); + + if (table_row_listener) { google.visualization.events.removeListener(table_row_listener); } + }); + + mainChart.on('success', function(response) { + var chart = response.chart; + var dataTable = response.dataTable; + + table_row_listener = google.visualization.events.addListener(chart, 'select', function() { + var options; + if (chart.getSelection().length) { + var row = chart.getSelection()[0].row; + var medium = dataTable.getValue(row, 0); + options = { + query: { + filters: 'ga:medium==' + medium, + }, + chart: { + options: { + title: medium, + } + } + }; + } else { + options = { + chart: { + options: { + title: 'All', + } + } + }; + delete breakdownChart.get().query.filters; + } + breakdownChart.set(options).execute(); + }); + }); + + // Add CSS animation to visually show the when users come and go. + activeUsers.once('success', function() { + var element = this.container.firstChild; + var timeout; + + this.on('change', function(data) { + element = this.container.firstChild; + var animationClass = data.delta > 0 ? 'is-increasing' : 'is-decreasing'; + element.className += (' ' + animationClass); + + clearTimeout(timeout); + timeout = setTimeout(function() { + element.className = element.className.replace(/ is-(increasing|decreasing)/g, ''); + }, 3000); + }); + }); + }, + + /* + * Credits to https://github.com/googleanalytics/ga-dev-tools + * This is the Active Users component that polls + * the number of active users on Analytics each 5 secs + */ + analytics_create_components: function() { + + gapi.analytics.createComponent('ActiveUsers', { + + initialize: function() { + this.activeUsers = 0; + gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); + }, + + execute: function() { + // Stop any polling currently going on. + if (this.polling_) { + this.stop(); + } + + this.render_(); + + // Wait until the user is authorized. + if (gapi.analytics.auth.isAuthorized()) { + this.pollActiveUsers_(); + } else { + gapi.analytics.auth.once('signIn', this.pollActiveUsers_.bind(this)); + } + }, + + stop: function() { + clearTimeout(this.timeout_); + this.polling_ = false; + this.emit('stop', {activeUsers: this.activeUsers}); + }, + + render_: function() { + var opts = this.get(); + + // Render the component inside the container. + this.container = typeof opts.container === 'string' ? + document.getElementById(opts.container) : opts.container; + + this.container.innerHTML = opts.template || this.template; + this.container.querySelector('b').innerHTML = this.activeUsers; + }, + + pollActiveUsers_: function() { + var options = this.get(); + var pollingInterval = (options.pollingInterval || 5) * 1000; + + if (isNaN(pollingInterval) || pollingInterval < 5000) { + throw new Error('Frequency must be 5 seconds or more.'); + } + + this.polling_ = true; + gapi.client.analytics.data.realtime + .get({ids:options.ids, metrics:'rt:activeUsers'}) + .then(function(response) { + var result = response.result; + var newValue = result.totalResults ? +result.rows[0][0] : 0; + var oldValue = this.activeUsers; + + this.emit('success', {activeUsers: this.activeUsers}); + + if (newValue !== oldValue) { + this.activeUsers = newValue; + this.onChange_(newValue - oldValue); + } + + if (this.polling_) { + this.timeout_ = setTimeout(this.pollActiveUsers_.bind(this), pollingInterval); + } + }.bind(this)); + }, + + onChange_: function(delta) { + var valueContainer = this.container.querySelector('b'); + if (valueContainer) { valueContainer.innerHTML = this.activeUsers; } + + this.emit('change', {activeUsers: this.activeUsers, delta: delta}); + if (delta > 0) { + this.emit('increase', {activeUsers: this.activeUsers, delta: delta}); + } else { + this.emit('decrease', {activeUsers: this.activeUsers, delta: delta}); + } + }, + + handleSignOut_: function() { + this.stop(); + gapi.analytics.auth.once('signIn', this.handleSignIn_.bind(this)); + }, + + handleSignIn_: function() { + this.pollActiveUsers_(); + gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); + }, + + template: + '<div class="ActiveUsers">' + + 'Active Users: <b class="ActiveUsers-value"></b>' + + '</div>' + + }); + }, + + // Utility functions + addLoader: function(selector) { + var loader = '<span class="fa fa-3x fa-spin fa-spinner fa-pulse"/>'; + selector.html("<div class='o_loader'>" + loader + "</div>"); + }, + getValue: function(d) { return d[1]; }, + format_number: function(value, type, digits, symbol) { + if (type === 'currency') { + return this.render_monetary_field(value, this.currency_id); + } else { + return field_utils.format[type](value || 0, {digits: digits}) + ' ' + symbol; + } + }, + formatValue: function (value) { + var formatter = field_utils.format.float; + var formatedValue = formatter(value, undefined, FORMAT_OPTIONS); + return formatedValue; + }, + render_monetary_field: function(value, currency_id) { + var currency = session.get_currency(currency_id); + var formatted_value = field_utils.format.float(value || 0, {digits: currency && currency.digits}); + if (currency) { + if (currency.position === "after") { + formatted_value += currency.symbol; + } else { + formatted_value = currency.symbol + formatted_value; + } + } + return formatted_value; + }, + +}); + +core.action_registry.add('backend_dashboard', Dashboard); + +return Dashboard; +}); diff --git a/addons/website/static/src/js/backend/res_config_settings.js b/addons/website/static/src/js/backend/res_config_settings.js new file mode 100644 index 00000000..6dd014c0 --- /dev/null +++ b/addons/website/static/src/js/backend/res_config_settings.js @@ -0,0 +1,81 @@ +odoo.define('website.settings', function (require) { + +const BaseSettingController = require('base.settings').Controller; +const core = require('web.core'); +const Dialog = require('web.Dialog'); +const FieldBoolean = require('web.basic_fields').FieldBoolean; +const fieldRegistry = require('web.field_registry'); +const FormController = require('web.FormController'); + +const QWeb = core.qweb; +const _t = core._t; + +BaseSettingController.include({ + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Bypasses the discard confirmation dialog when going to a website because + * the target website will be the one selected and when selecting a theme + * because the theme will be installed on the selected website. + * + * Without this override, it is impossible to go to a website other than the + * first because discarding will revert it back to the default value. + * + * Without this override, it is impossible to edit robots.txt website other than the + * first because discarding will revert it back to the default value. + * + * Without this override, it is impossible to submit sitemap to google other than for the + * first website because discarding will revert it back to the default value. + * + * Without this override, it is impossible to install a theme on a website + * other than the first because discarding will revert it back to the + * default value. + * + * @override + */ + _onButtonClicked: function (ev) { + if (ev.data.attrs.name === 'website_go_to' + || ev.data.attrs.name === 'action_open_robots' + || ev.data.attrs.name === 'action_ping_sitemap' + || ev.data.attrs.name === 'install_theme_on_current_website') { + FormController.prototype._onButtonClicked.apply(this, arguments); + } else { + this._super.apply(this, arguments); + } + }, +}); + +const WebsiteCookiesbarField = FieldBoolean.extend({ + xmlDependencies: ['/website/static/src/xml/website.res_config_settings.xml'], + + _onChange: function () { + const checked = this.$input[0].checked; + if (!checked) { + return this._setValue(checked); + } + + const cancelCallback = () => this.$input[0].checked = !checked; + Dialog.confirm(this, null, { + title: _t("Please confirm"), + $content: QWeb.render('website.res_config_settings.cookies_modal_main'), + buttons: [{ + text: _t('Do not activate'), + classes: 'btn-primary', + close: true, + click: cancelCallback, + }, + { + text: _t('Activate anyway'), + close: true, + click: () => this._setValue(checked), + }], + cancel_callback: cancelCallback, + }); + }, +}); + +fieldRegistry.add('website_cookiesbar_field', WebsiteCookiesbarField); +}); diff --git a/addons/website/static/src/js/content/compatibility.js b/addons/website/static/src/js/content/compatibility.js new file mode 100644 index 00000000..f4148802 --- /dev/null +++ b/addons/website/static/src/js/content/compatibility.js @@ -0,0 +1,38 @@ +odoo.define('website.content.compatibility', function (require) { +'use strict'; + +/** + * Tweaks the website rendering so that the old browsers correctly render the + * content too. + */ + +require('web.dom_ready'); + +// Check the browser and its version and add the info as an attribute of the +// HTML element so that css selectors can match it +var browser = _.findKey($.browser, function (v) { return v === true; }); +if ($.browser.mozilla && +$.browser.version.replace(/^([0-9]+\.[0-9]+).*/, '\$1') < 20) { + browser = 'msie'; +} +browser += (',' + $.browser.version); +var mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i; +if (mobileRegex.test(window.navigator.userAgent.toLowerCase())) { + browser += ',mobile'; +} +document.documentElement.setAttribute('data-browser', browser); + +// Check if flex is supported and add the info as an attribute of the HTML +// element so that css selectors can match it (only if not supported) +var htmlStyle = document.documentElement.style; +var isFlexSupported = (('flexWrap' in htmlStyle) + || ('WebkitFlexWrap' in htmlStyle) + || ('msFlexWrap' in htmlStyle)); +if (!isFlexSupported) { + document.documentElement.setAttribute('data-no-flex', ''); +} + +return { + browser: browser, + isFlexSupported: isFlexSupported, +}; +}); diff --git a/addons/website/static/src/js/content/menu.js b/addons/website/static/src/js/content/menu.js new file mode 100644 index 00000000..71f74ab5 --- /dev/null +++ b/addons/website/static/src/js/content/menu.js @@ -0,0 +1,642 @@ +odoo.define('website.content.menu', function (require) { +'use strict'; + +const config = require('web.config'); +var dom = require('web.dom'); +var publicWidget = require('web.public.widget'); +var wUtils = require('website.utils'); +var animations = require('website.content.snippets.animation'); + +const extraMenuUpdateCallbacks = []; + +const BaseAnimatedHeader = animations.Animation.extend({ + disabledInEditableMode: false, + effects: [{ + startEvents: 'scroll', + update: '_updateHeaderOnScroll', + }, { + startEvents: 'resize', + update: '_updateHeaderOnResize', + }], + + /** + * @constructor + */ + init: function () { + this._super(...arguments); + this.fixedHeader = false; + this.scrolledPoint = 0; + this.hasScrolled = false; + }, + /** + * @override + */ + start: function () { + this.$main = this.$el.next('main'); + this.isOverlayHeader = !!this.$el.closest('.o_header_overlay, .o_header_overlay_theme').length; + this.$dropdowns = this.$el.find('.dropdown, .dropdown-menu'); + this.$navbarCollapses = this.$el.find('.navbar-collapse'); + + // While scrolling through navbar menus on medium devices, body should not be scrolled with it + this.$navbarCollapses.on('show.bs.collapse.BaseAnimatedHeader', function () { + if (config.device.size_class <= config.device.SIZES.SM) { + $(document.body).addClass('overflow-hidden'); + } + }).on('hide.bs.collapse.BaseAnimatedHeader', function () { + $(document.body).removeClass('overflow-hidden'); + }); + + // We can rely on transitionend which is well supported but not on + // transitionstart, so we listen to a custom odoo event. + this._transitionCount = 0; + this.$el.on('odoo-transitionstart.BaseAnimatedHeader', () => this._adaptToHeaderChangeLoop(1)); + this.$el.on('transitionend.BaseAnimatedHeader', () => this._adaptToHeaderChangeLoop(-1)); + + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this._toggleFixedHeader(false); + this.$el.removeClass('o_header_affixed o_header_is_scrolled o_header_no_transition'); + this.$navbarCollapses.off('.BaseAnimatedHeader'); + this.$el.off('.BaseAnimatedHeader'); + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptFixedHeaderPosition() { + dom.compensateScrollbar(this.el, this.fixedHeader, false, 'right'); + }, + /** + * @private + */ + _adaptToHeaderChange: function () { + this._updateMainPaddingTop(); + this.el.classList.toggle('o_top_fixed_element', this.fixedHeader && this._isShown()); + + for (const callback of extraMenuUpdateCallbacks) { + callback(); + } + }, + /** + * @private + * @param {integer} [addCount=0] + */ + _adaptToHeaderChangeLoop: function (addCount = 0) { + this._adaptToHeaderChange(); + + this._transitionCount += addCount; + this._transitionCount = Math.max(0, this._transitionCount); + + // As long as we detected a transition start without its related + // transition end, keep updating the main padding top. + if (this._transitionCount > 0) { + window.requestAnimationFrame(() => this._adaptToHeaderChangeLoop()); + + // The normal case would be to have the transitionend event to be + // fired but we cannot rely on it, so we use a timeout as fallback. + if (addCount !== 0) { + clearTimeout(this._changeLoopTimer); + this._changeLoopTimer = setTimeout(() => { + this._adaptToHeaderChangeLoop(-this._transitionCount); + }, 500); + } + } else { + // When we detected all transitionend events, we need to stop the + // setTimeout fallback. + clearTimeout(this._changeLoopTimer); + } + }, + /** + * @private + */ + _computeTopGap() { + return 0; + }, + /** + * @private + */ + _isShown() { + return true; + }, + /** + * @private + * @param {boolean} [useFixed=true] + */ + _toggleFixedHeader: function (useFixed = true) { + this.fixedHeader = useFixed; + this._adaptToHeaderChange(); + this.el.classList.toggle('o_header_affixed', useFixed); + this._adaptFixedHeaderPosition(); + }, + /** + * @private + */ + _updateMainPaddingTop: function () { + this.headerHeight = this.$el.outerHeight(); + this.topGap = this._computeTopGap(); + + if (this.isOverlayHeader) { + return; + } + this.$main.css('padding-top', this.fixedHeader ? this.headerHeight : ''); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the window is scrolled + * + * @private + * @param {integer} scroll + */ + _updateHeaderOnScroll: function (scroll) { + // Disable css transition if refresh with scrollTop > 0 + if (!this.hasScrolled) { + this.hasScrolled = true; + if (scroll > 0) { + this.$el.addClass('o_header_no_transition'); + } + } else { + this.$el.removeClass('o_header_no_transition'); + } + + // Indicates the page is scrolled, the logo size is changed. + const headerIsScrolled = (scroll > this.scrolledPoint); + if (this.headerIsScrolled !== headerIsScrolled) { + this.el.classList.toggle('o_header_is_scrolled', headerIsScrolled); + this.$el.trigger('odoo-transitionstart'); + this.headerIsScrolled = headerIsScrolled; + } + + // Close opened menus + this.$dropdowns.removeClass('show'); + this.$navbarCollapses.removeClass('show').attr('aria-expanded', false); + }, + /** + * Called when the window is resized + * + * @private + */ + _updateHeaderOnResize: function () { + this._adaptFixedHeaderPosition(); + if (document.body.classList.contains('overflow-hidden') + && config.device.size_class > config.device.SIZES.SM) { + document.body.classList.remove('overflow-hidden'); + this.$el.find('.navbar-collapse').removeClass('show'); + } + }, +}); + +publicWidget.registry.StandardAffixedHeader = BaseAnimatedHeader.extend({ + selector: 'header.o_header_standard:not(.o_header_sidebar)', + + /** + * @constructor + */ + init: function () { + this._super(...arguments); + this.fixedHeaderShow = false; + this.scrolledPoint = 300; + }, + /** + * @override + */ + start: function () { + this.headerHeight = this.$el.outerHeight(); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _isShown() { + return !this.fixedHeader || this.fixedHeaderShow; + }, + /** + * Called when the window is scrolled + * + * @private + * @param {integer} scroll + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + + const mainPosScrolled = (scroll > this.headerHeight + this.topGap); + const reachPosScrolled = (scroll > this.scrolledPoint + this.topGap); + + // Switch between static/fixed position of the header + if (this.fixedHeader !== mainPosScrolled) { + this.$el.css('transform', mainPosScrolled ? 'translate(0, -100%)' : ''); + void this.$el[0].offsetWidth; // Force a paint refresh + this._toggleFixedHeader(mainPosScrolled); + } + // Show/hide header + if (this.fixedHeaderShow !== reachPosScrolled) { + this.$el.css('transform', reachPosScrolled ? `translate(0, -${this.topGap}px)` : 'translate(0, -100%)'); + this.fixedHeaderShow = reachPosScrolled; + this._adaptToHeaderChange(); + } + }, +}); + +publicWidget.registry.FixedHeader = BaseAnimatedHeader.extend({ + selector: 'header.o_header_fixed', + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + // Need to be 'unfixed' when the window is not scrolled so that the + // transparent menu option still works. + if (scroll > (this.scrolledPoint + this.topGap)) { + if (!this.$el.hasClass('o_header_affixed')) { + this.$el.css('transform', `translate(0, -${this.topGap}px)`); + void this.$el[0].offsetWidth; // Force a paint refresh + this._toggleFixedHeader(true); + } + } else { + this._toggleFixedHeader(false); + void this.$el[0].offsetWidth; // Force a paint refresh + this.$el.css('transform', ''); + } + }, +}); + +const BaseDisappearingHeader = publicWidget.registry.FixedHeader.extend({ + /** + * @override + */ + init: function () { + this._super(...arguments); + this.scrollingDownwards = true; + this.hiddenHeader = false; + this.position = 0; + this.atTop = true; + this.checkPoint = 0; + this.scrollOffsetLimit = 200; + }, + /** + * @override + */ + destroy: function () { + this._showHeader(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _hideHeader: function () { + this.$el.trigger('odoo-transitionstart'); + }, + /** + * @override + */ + _isShown() { + return !this.fixedHeader || !this.hiddenHeader; + }, + /** + * @private + */ + _showHeader: function () { + this.$el.trigger('odoo-transitionstart'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _updateHeaderOnScroll: function (scroll) { + this._super(...arguments); + + const scrollingDownwards = (scroll > this.position); + const atTop = (scroll <= 0); + if (scrollingDownwards !== this.scrollingDownwards) { + this.checkPoint = scroll; + } + + this.scrollingDownwards = scrollingDownwards; + this.position = scroll; + this.atTop = atTop; + + if (scrollingDownwards) { + if (!this.hiddenHeader && scroll - this.checkPoint > (this.scrollOffsetLimit + this.topGap)) { + this.hiddenHeader = true; + this._hideHeader(); + } + } else { + if (this.hiddenHeader && scroll - this.checkPoint < -(this.scrollOffsetLimit + this.topGap) / 2) { + this.hiddenHeader = false; + this._showHeader(); + } + } + + if (atTop && !this.atTop) { + // Force reshowing the invisible-on-scroll sections when reaching + // the top again + this._showHeader(); + } + }, +}); + +publicWidget.registry.DisappearingHeader = BaseDisappearingHeader.extend({ + selector: 'header.o_header_disappears', + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _hideHeader: function () { + this._super(...arguments); + this.$el.css('transform', 'translate(0, -100%)'); + }, + /** + * @override + */ + _showHeader: function () { + this._super(...arguments); + this.$el.css('transform', this.atTop ? '' : `translate(0, -${this.topGap}px)`); + }, +}); + +publicWidget.registry.FadeOutHeader = BaseDisappearingHeader.extend({ + selector: 'header.o_header_fade_out', + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _hideHeader: function () { + this._super(...arguments); + this.$el.stop(false, true).fadeOut(); + }, + /** + * @override + */ + _showHeader: function () { + this._super(...arguments); + this.$el.css('transform', this.atTop ? '' : `translate(0, -${this.topGap}px)`); + this.$el.stop(false, true).fadeIn(); + }, +}); + +/** + * Auto adapt the header layout so that elements are not wrapped on a new line. + */ +publicWidget.registry.autohideMenu = publicWidget.Widget.extend({ + selector: 'header#top', + disabledInEditableMode: false, + + /** + * @override + */ + async start() { + await this._super(...arguments); + this.$topMenu = this.$('#top_menu'); + this.noAutohide = this.$el.is('.o_no_autohide_menu'); + if (!this.noAutohide) { + await wUtils.onceAllImagesLoaded(this.$('.navbar'), this.$('.o_mega_menu, .o_offcanvas_logo_container, .dropdown-menu .o_lang_flag')); + + // The previous code will make sure we wait for images to be fully + // loaded before initializing the auto more menu. But in some cases, + // it is not enough, we also have to wait for fonts or even extra + // scripts. Those will have no impact on the feature in most cases + // though, so we will only update the auto more menu at that time, + // no wait for it to initialize the feature. + var $window = $(window); + $window.on('load.autohideMenu', function () { + $window.trigger('resize'); + }); + + dom.initAutoMoreMenu(this.$topMenu, {unfoldable: '.divider, .divider ~ li, .o_no_autohide_item'}); + } + this.$topMenu.removeClass('o_menu_loading'); + this.$topMenu.trigger('menu_loaded'); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + if (!this.noAutohide && this.$topMenu) { + $(window).off('.autohideMenu'); + dom.destroyAutoMoreMenu(this.$topMenu); + } + }, +}); + +/** + * Note: this works well with the affixMenu... by chance (menuDirection is + * called after alphabetically). + * + * @todo check bootstrap v4: maybe handled automatically now ? + */ +publicWidget.registry.menuDirection = publicWidget.Widget.extend({ + selector: 'header .navbar .nav', + disabledInEditableMode: false, + events: { + 'show.bs.dropdown': '_onDropdownShow', + }, + + /** + * @override + */ + start: function () { + this.defaultAlignment = this.$el.is('.ml-auto, .ml-auto ~ *') ? 'right' : 'left'; + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} alignment - either 'left' or 'right' + * @param {integer} liOffset + * @param {integer} liWidth + * @param {integer} menuWidth + * @param {integer} pageWidth + * @returns {boolean} + */ + _checkOpening: function (alignment, liOffset, liWidth, menuWidth, pageWidth) { + if (alignment === 'left') { + // Check if ok to open the dropdown to the right (no window overflow) + return (liOffset + menuWidth <= pageWidth); + } else { + // Check if ok to open the dropdown to the left (no window overflow) + return (liOffset + liWidth - menuWidth >= 0); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onDropdownShow: function (ev) { + var $li = $(ev.target); + var $menu = $li.children('.dropdown-menu'); + var liOffset = $li.offset().left; + var liWidth = $li.outerWidth(); + var menuWidth = $menu.outerWidth(); + var pageWidth = $('#wrapwrap').outerWidth(); + + $menu.removeClass('dropdown-menu-left dropdown-menu-right'); + + var alignment = this.defaultAlignment; + if ($li.nextAll(':visible').length === 0) { + // The dropdown is the last menu item, open to the left + alignment = 'right'; + } + + // If can't open in the current direction because it would overflow the + // page, change the direction. But if the other direction would do the + // same, change back the direction. + for (var i = 0; i < 2; i++) { + if (!this._checkOpening(alignment, liOffset, liWidth, menuWidth, pageWidth)) { + alignment = (alignment === 'left' ? 'right' : 'left'); + } + } + + $menu.addClass('dropdown-menu-' + alignment); + }, +}); + +publicWidget.registry.hoverableDropdown = animations.Animation.extend({ + selector: 'header.o_hoverable_dropdown', + disabledInEditableMode: false, + effects: [{ + startEvents: 'resize', + update: '_dropdownHover', + }], + events: { + 'mouseenter .dropdown': '_onMouseEnter', + 'mouseleave .dropdown': '_onMouseLeave', + }, + + /** + * @override + */ + start: function () { + this.$dropdownMenus = this.$el.find('.dropdown-menu'); + this.$dropdownToggles = this.$el.find('.dropdown-toggle'); + this._dropdownHover(); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _dropdownHover: function () { + if (config.device.size_class > config.device.SIZES.SM) { + this.$dropdownMenus.css('margin-top', '0'); + this.$dropdownMenus.css('top', 'unset'); + } else { + this.$dropdownMenus.css('margin-top', ''); + this.$dropdownMenus.css('top', ''); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onMouseEnter: function (ev) { + if (config.device.size_class <= config.device.SIZES.SM) { + return; + } + + const $dropdown = $(ev.currentTarget); + $dropdown.addClass('show'); + $dropdown.find(this.$dropdownToggles).attr('aria-expanded', 'true'); + $dropdown.find(this.$dropdownMenus).addClass('show'); + }, + /** + * @private + * @param {Event} ev + */ + _onMouseLeave: function (ev) { + if (config.device.size_class <= config.device.SIZES.SM) { + return; + } + + const $dropdown = $(ev.currentTarget); + $dropdown.removeClass('show'); + $dropdown.find(this.$dropdownToggles).attr('aria-expanded', 'false'); + $dropdown.find(this.$dropdownMenus).removeClass('show'); + }, +}); + +publicWidget.registry.HeaderMainCollapse = publicWidget.Widget.extend({ + selector: 'header#top', + events: { + 'show.bs.collapse #top_menu_collapse': '_onCollapseShow', + 'hidden.bs.collapse #top_menu_collapse': '_onCollapseHidden', + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onCollapseShow() { + this.el.classList.add('o_top_menu_collapse_shown'); + }, + /** + * @private + */ + _onCollapseHidden() { + this.el.classList.remove('o_top_menu_collapse_shown'); + }, +}); + +return { + extraMenuUpdateCallbacks: extraMenuUpdateCallbacks, +}; +}); diff --git a/addons/website/static/src/js/content/ripple_effect.js b/addons/website/static/src/js/content/ripple_effect.js new file mode 100644 index 00000000..2e61d5b7 --- /dev/null +++ b/addons/website/static/src/js/content/ripple_effect.js @@ -0,0 +1,72 @@ +odoo.define('website.ripple_effect', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); + +publicWidget.registry.RippleEffect = publicWidget.Widget.extend({ + selector: '.btn, .dropdown-toggle, .dropdown-item', + events: { + 'click': '_onClick', + }, + duration: 350, + + /** + * @override + */ + start: async function () { + this.diameter = Math.max(this.$el.outerWidth(), this.$el.outerHeight()); + this.offsetX = this.$el.offset().left; + this.offsetY = this.$el.offset().top; + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this._super(...arguments); + if (this.rippleEl) { + this.rippleEl.remove(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {boolean} [toggle] + */ + _toggleRippleEffect: function (toggle) { + this.el.classList.toggle('o_js_ripple_effect', toggle); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onClick: function (ev) { + if (!this.rippleEl) { + this.rippleEl = document.createElement('span'); + this.rippleEl.classList.add('o_ripple_item'); + this.rippleEl.style.animationDuration = `${this.duration}ms`; + this.rippleEl.style.width = `${this.diameter}px`; + this.rippleEl.style.height = `${this.diameter}px`; + this.el.appendChild(this.rippleEl); + } + + clearTimeout(this.timeoutID); + this._toggleRippleEffect(false); + + this.rippleEl.style.top = `${ev.pageY - this.offsetY - this.diameter / 2}px`; + this.rippleEl.style.left = `${ev.pageX - this.offsetX - this.diameter / 2}px`; + + this._toggleRippleEffect(true); + this.timeoutID = setTimeout(() => this._toggleRippleEffect(false), this.duration); + }, +}); +}); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js new file mode 100644 index 00000000..5eb08630 --- /dev/null +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -0,0 +1,1092 @@ +odoo.define('website.content.snippets.animation', function (require) { +'use strict'; + +/** + * Provides a way to start JS code for snippets' initialization and animations. + */ + +var Class = require('web.Class'); +var config = require('web.config'); +var core = require('web.core'); +const dom = require('web.dom'); +var mixins = require('web.mixins'); +var publicWidget = require('web.public.widget'); +var utils = require('web.utils'); + +var qweb = core.qweb; + +// Initialize fallbacks for the use of requestAnimationFrame, +// cancelAnimationFrame and performance.now() +window.requestAnimationFrame = window.requestAnimationFrame + || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame + || window.msRequestAnimationFrame + || window.oRequestAnimationFrame; +window.cancelAnimationFrame = window.cancelAnimationFrame + || window.webkitCancelAnimationFrame + || window.mozCancelAnimationFrame + || window.msCancelAnimationFrame + || window.oCancelAnimationFrame; +if (!window.performance || !window.performance.now) { + window.performance = { + now: function () { + return Date.now(); + } + }; +} + +/** + * Add the notion of edit mode to public widgets. + */ +publicWidget.Widget.include({ + /** + * Indicates if the widget should not be instantiated in edit. The default + * is true, indeed most (all?) defined widgets only want to initialize + * events and states which should not be active in edit mode (this is + * especially true for non-website widgets). + * + * @type {boolean} + */ + disabledInEditableMode: true, + /** + * Acts as @see Widget.events except that the events are only binded if the + * Widget instance is instanciated in edit mode. The property is not + * considered if @see disabledInEditableMode is false. + */ + edit_events: null, + /** + * Acts as @see Widget.events except that the events are only binded if the + * Widget instance is instanciated in readonly mode. The property only + * makes sense if @see disabledInEditableMode is false, you should simply + * use @see Widget.events otherwise. + */ + read_events: null, + + /** + * Initializes the events that will need to be binded according to the + * given mode. + * + * @constructor + * @param {Object} parent + * @param {Object} [options] + * @param {boolean} [options.editableMode=false] + * true if the page is in edition mode + */ + init: function (parent, options) { + this._super.apply(this, arguments); + + this.editableMode = this.options.editableMode || false; + var extraEvents = this.editableMode ? this.edit_events : this.read_events; + if (extraEvents) { + this.events = _.extend({}, this.events || {}, extraEvents); + } + }, +}); + +/** + * In charge of handling one animation loop using the requestAnimationFrame + * feature. This is used by the `Animation` class below and should not be called + * directly by an end developer. + * + * This uses a simple API: it can be started, stopped, played and paused. + */ +var AnimationEffect = Class.extend(mixins.ParentedMixin, { + /** + * @constructor + * @param {Object} parent + * @param {function} updateCallback - the animation update callback + * @param {string} [startEvents=scroll] + * space separated list of events which starts the animation loop + * @param {jQuery|DOMElement} [$startTarget=window] + * the element(s) on which the startEvents are listened + * @param {Object} [options] + * @param {function} [options.getStateCallback] + * a function which returns a value which represents the state of the + * animation, i.e. for two same value, no refreshing of the animation + * is needed. Can be used for optimization. If the $startTarget is + * the window element, this defaults to returning the current + * scoll offset of the window or the size of the window for the + * scroll and resize events respectively. + * @param {string} [options.endEvents] + * space separated list of events which pause the animation loop. If + * not given, the animation is stopped after a while (if no + * startEvents is received again) + * @param {jQuery|DOMElement} [options.$endTarget=$startTarget] + * the element(s) on which the endEvents are listened + */ + init: function (parent, updateCallback, startEvents, $startTarget, options) { + mixins.ParentedMixin.init.call(this); + this.setParent(parent); + + options = options || {}; + this._minFrameTime = 1000 / (options.maxFPS || 100); + + // Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks + this._updateCallback = updateCallback; + this.startEvents = startEvents || 'scroll'; + const mainScrollingElement = $().getScrollingElement()[0]; + const mainScrollingTarget = mainScrollingElement === document.documentElement ? window : mainScrollingElement; + this.$startTarget = $($startTarget ? $startTarget : this.startEvents === 'scroll' ? mainScrollingTarget : window); + if (options.getStateCallback) { + this._getStateCallback = options.getStateCallback; + } else if (this.startEvents === 'scroll' && this.$startTarget[0] === mainScrollingTarget) { + const $scrollable = this.$startTarget; + this._getStateCallback = function () { + return $scrollable.scrollTop(); + }; + } else if (this.startEvents === 'resize' && this.$startTarget[0] === window) { + this._getStateCallback = function () { + return { + width: window.innerWidth, + height: window.innerHeight, + }; + }; + } else { + this._getStateCallback = function () { + return undefined; + }; + } + this.endEvents = options.endEvents || false; + this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget; + + this._updateCallback = this._updateCallback.bind(parent); + this._getStateCallback = this._getStateCallback.bind(parent); + + // Add a namespace to events using the generated uid + this._uid = '_animationEffect' + _.uniqueId(); + this.startEvents = _processEvents(this.startEvents, this._uid); + if (this.endEvents) { + this.endEvents = _processEvents(this.endEvents, this._uid); + } + + function _processEvents(events, namespace) { + events = events.split(' '); + return _.each(events, function (e, index) { + events[index] += ('.' + namespace); + }).join(' '); + } + }, + /** + * @override + */ + destroy: function () { + mixins.ParentedMixin.destroy.call(this); + this.stop(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Initializes when the animation must be played and paused and initializes + * the animation first frame. + */ + start: function () { + // Initialize the animation first frame + this._paused = false; + this._rafID = window.requestAnimationFrame((function (t) { + this._update(t); + this._paused = true; + }).bind(this)); + + // Initialize the animation play/pause events + if (this.endEvents) { + /** + * If there are endEvents, the animation should begin playing when + * the startEvents are triggered on the $startTarget and pause when + * the endEvents are triggered on the $endTarget. + */ + this.$startTarget.on(this.startEvents, (function (e) { + if (this._paused) { + _.defer(this.play.bind(this, e)); + } + }).bind(this)); + this.$endTarget.on(this.endEvents, (function () { + if (!this._paused) { + _.defer(this.pause.bind(this)); + } + }).bind(this)); + } else { + /** + * Else, if there is no endEvents, the animation should begin playing + * when the startEvents are *continuously* triggered on the + * $startTarget or fully played once. To achieve this, the animation + * begins playing and is scheduled to pause after 2 seconds. If the + * startEvents are triggered during that time, this is not paused + * for another 2 seconds. This allows to describe an "effect" + * animation (which lasts less than 2 seconds) or an animation which + * must be playing *during* an event (scroll, mousemove, resize, + * repeated clicks, ...). + */ + var pauseTimer = null; + this.$startTarget.on(this.startEvents, _.throttle((function (e) { + this.play(e); + + clearTimeout(pauseTimer); + pauseTimer = _.delay((function () { + this.pause(); + pauseTimer = null; + }).bind(this), 2000); + }).bind(this), 250, {trailing: false})); + } + }, + /** + * Pauses the animation and destroys the attached events which trigger the + * animation to be played or paused. + */ + stop: function () { + this.$startTarget.off(this.startEvents); + if (this.endEvents) { + this.$endTarget.off(this.endEvents); + } + this.pause(); + }, + /** + * Forces the requestAnimationFrame loop to start. + * + * @param {Event} e - the event which triggered the animation to play + */ + play: function (e) { + this._newEvent = e; + if (!this._paused) { + return; + } + this._paused = false; + this._rafID = window.requestAnimationFrame(this._update.bind(this)); + this._lastUpdateTimestamp = undefined; + }, + /** + * Forces the requestAnimationFrame loop to stop. + */ + pause: function () { + if (this._paused) { + return; + } + this._paused = true; + window.cancelAnimationFrame(this._rafID); + this._lastUpdateTimestamp = undefined; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Callback which is repeatedly called by the requestAnimationFrame loop. + * It controls the max fps at which the animation is running and initializes + * the values that the update callback needs to describe the animation + * (state, elapsedTime, triggered event). + * + * @private + * @param {DOMHighResTimeStamp} timestamp + */ + _update: function (timestamp) { + if (this._paused) { + return; + } + this._rafID = window.requestAnimationFrame(this._update.bind(this)); + + // Check the elapsed time since the last update callback call. + // Consider it 0 if there is no info of last timestamp and leave this + // _update call if it was called too soon (would overflow the set max FPS). + var elapsedTime = 0; + if (this._lastUpdateTimestamp) { + elapsedTime = timestamp - this._lastUpdateTimestamp; + if (elapsedTime < this._minFrameTime) { + return; + } + } + + // Check the new animation state thanks to the get state callback and + // store its new value. If the state is the same as the previous one, + // leave this _update call, except if there is an event which triggered + // the "play" method again. + var animationState = this._getStateCallback(elapsedTime, this._newEvent); + if (!this._newEvent + && animationState !== undefined + && _.isEqual(animationState, this._animationLastState)) { + return; + } + this._animationLastState = animationState; + + // Call the update callback with frame parameters + this._updateCallback(this._animationLastState, elapsedTime, this._newEvent); + this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called + this._newEvent = undefined; // Forget the event which triggered the last "play" call + }, +}); + +/** + * Also register AnimationEffect automatically (@see effects, _prepareEffects). + */ +var Animation = publicWidget.Widget.extend({ + /** + * The max FPS at which all the automatic animation effects will be + * running by default. + */ + maxFPS: 100, + /** + * @see this._prepareEffects + * + * @type {Object[]} + * @type {string} startEvents + * The names of the events which trigger the effect to begin playing. + * @type {string} [startTarget] + * A selector to find the target where to listen for the start events + * (if no selector, the window target will be used). If the whole + * $target of the animation should be used, use the 'selector' string. + * @type {string} [endEvents] + * The name of the events which trigger the end of the effect (if none + * is defined, the animation will stop after a while + * @see AnimationEffect.start). + * @type {string} [endTarget] + * A selector to find the target where to listen for the end events + * (if no selector, the startTarget will be used). If the whole + * $target of the animation should be used, use the 'selector' string. + * @type {string} update + * A string which refers to a method which will be used as the update + * callback for the effect. It receives 3 arguments: the animation + * state, the elapsedTime since last update and the event which + * triggered the animation (undefined if just a new update call + * without trigger). + * @type {string} [getState] + * The animation state is undefined by default, the scroll offset for + * the particular {startEvents: 'scroll'} effect and an object with + * width and height for the particular {startEvents: 'resize'} effect. + * There is the possibility to define the getState callback of the + * animation effect with this key. This allows to improve performance + * even further in some cases. + */ + effects: [], + + /** + * Initializes the animation. The method should not be called directly as + * called automatically on animation instantiation and on restart. + * + * Also, prepares animation's effects and start them if any. + * + * @override + */ + start: function () { + this._prepareEffects(); + _.each(this._animationEffects, function (effect) { + effect.start(); + }); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Registers `AnimationEffect` instances. + * + * This can be done by extending this method and calling the @see _addEffect + * method in it or, better, by filling the @see effects property. + * + * @private + */ + _prepareEffects: function () { + this._animationEffects = []; + + var self = this; + _.each(this.effects, function (desc) { + self._addEffect(self[desc.update], desc.startEvents, _findTarget(desc.startTarget), { + getStateCallback: desc.getState && self[desc.getState], + endEvents: desc.endEvents || undefined, + $endTarget: _findTarget(desc.endTarget), + maxFPS: self.maxFPS, + }); + + // Return the DOM element matching the selector in the form + // described above. + function _findTarget(selector) { + if (selector) { + if (selector === 'selector') { + return self.$target; + } + return self.$(selector); + } + return undefined; + } + }); + }, + /** + * Registers a new `AnimationEffect` according to given parameters. + * + * @private + * @see AnimationEffect.init + */ + _addEffect: function (updateCallback, startEvents, $startTarget, options) { + this._animationEffects.push( + new AnimationEffect(this, updateCallback, startEvents, $startTarget, options) + ); + }, +}); + +//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +var registry = publicWidget.registry; + +registry.slider = publicWidget.Widget.extend({ + selector: '.carousel', + disabledInEditableMode: false, + edit_events: { + 'content_changed': '_onContentChanged', + }, + + /** + * @override + */ + start: function () { + this.$('img').on('load.slider', () => this._computeHeights()); + this._computeHeights(); + // Initialize carousel and pause if in edit mode. + this.$target.carousel(this.editableMode ? 'pause' : undefined); + $(window).on('resize.slider', _.debounce(() => this._computeHeights(), 250)); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + this.$('img').off('.slider'); + this.$target.carousel('pause'); + this.$target.removeData('bs.carousel'); + _.each(this.$('.carousel-item'), function (el) { + $(el).css('min-height', ''); + }); + $(window).off('.slider'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _computeHeights: function () { + var maxHeight = 0; + var $items = this.$('.carousel-item'); + $items.css('min-height', ''); + _.each($items, function (el) { + var $item = $(el); + var isActive = $item.hasClass('active'); + $item.addClass('active'); + var height = $item.outerHeight(); + if (height > maxHeight) { + maxHeight = height; + } + $item.toggleClass('active', isActive); + }); + $items.css('min-height', maxHeight); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onContentChanged: function (ev) { + this._computeHeights(); + }, +}); + +registry.Parallax = Animation.extend({ + selector: '.parallax', + disabledInEditableMode: false, + effects: [{ + startEvents: 'scroll', + update: '_onWindowScroll', + }], + + /** + * @override + */ + start: function () { + this._rebuild(); + $(window).on('resize.animation_parallax', _.debounce(this._rebuild.bind(this), 500)); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + $(window).off('.animation_parallax'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Prepares the background element which will scroll at a different speed + * according to the viewport dimensions and other snippet parameters. + * + * @private + */ + _rebuild: function () { + // Add/find bg DOM element to hold the parallax bg (support old v10.0 parallax) + this.$bg = this.$('> .s_parallax_bg'); + + // Get parallax speed + this.speed = parseFloat(this.$target.attr('data-scroll-background-ratio') || 0); + + // Reset offset if parallax effect will not be performed and leave + var noParallaxSpeed = (this.speed === 0 || this.speed === 1); + if (noParallaxSpeed) { + this.$bg.css({ + transform: '', + top: '', + bottom: '', + }); + return; + } + + // Initialize parallax data according to snippet and viewport dimensions + this.viewport = document.body.clientHeight - $('#wrapwrap').position().top; + this.visibleArea = [this.$target.offset().top]; + this.visibleArea.push(this.visibleArea[0] + this.$target.innerHeight() + this.viewport); + this.ratio = this.speed * (this.viewport / 10); + + // Provide a "safe-area" to limit parallax + const absoluteRatio = Math.abs(this.ratio); + this.$bg.css({ + top: -absoluteRatio, + bottom: -absoluteRatio, + }); + }, + + //-------------------------------------------------------------------------- + // Effects + //-------------------------------------------------------------------------- + + /** + * Describes how to update the snippet when the window scrolls. + * + * @private + * @param {integer} scrollOffset + */ + _onWindowScroll: function (scrollOffset) { + // Speed == 0 is no effect and speed == 1 is handled by CSS only + if (this.speed === 0 || this.speed === 1) { + return; + } + + // Perform translation if the element is visible only + var vpEndOffset = scrollOffset + this.viewport; + if (vpEndOffset >= this.visibleArea[0] + && vpEndOffset <= this.visibleArea[1]) { + this.$bg.css('transform', 'translateY(' + _getNormalizedPosition.call(this, vpEndOffset) + 'px)'); + } + + function _getNormalizedPosition(pos) { + // Normalize scroll in a 1 to 0 range + var r = (pos - this.visibleArea[1]) / (this.visibleArea[0] - this.visibleArea[1]); + // Normalize accordingly to current options + return Math.round(this.ratio * (2 * r - 1)); + } + }, +}); + +registry.mediaVideo = publicWidget.Widget.extend({ + selector: '.media_iframe_video', + + /** + * @override + */ + start: function () { + // TODO: this code should be refactored to make more sense and be better + // integrated with Odoo (this refactoring should be done in master). + + var def = this._super.apply(this, arguments); + if (this.$target.children('iframe').length) { + // There already is an <iframe/>, do nothing + return def; + } + + // Bug fix / compatibility: empty the <div/> element as all information + // to rebuild the iframe should have been saved on the <div/> element + this.$target.empty(); + + // Add extra content for size / edition + this.$target.append( + '<div class="css_editable_mode_display"> </div>' + + '<div class="media_iframe_video_size"> </div>' + ); + + // Rebuild the iframe. Depending on version / compatibility / instance, + // the src is saved in the 'data-src' attribute or the + // 'data-oe-expression' one (the latter is used as a workaround in 10.0 + // system but should obviously be reviewed in master). + this.$target.append($('<iframe/>', { + src: _.escape(this.$target.data('oe-expression') || this.$target.data('src')), + frameborder: '0', + allowfullscreen: 'allowfullscreen', + sandbox: 'allow-scripts allow-same-origin', // https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/ + })); + + return def; + }, +}); + +registry.backgroundVideo = publicWidget.Widget.extend({ + selector: '.o_background_video', + xmlDependencies: ['/website/static/src/xml/website.background.video.xml'], + disabledInEditableMode: false, + + /** + * @override + */ + start: function () { + var proms = [this._super(...arguments)]; + + this.videoSrc = this.el.dataset.bgVideoSrc; + this.iframeID = _.uniqueId('o_bg_video_iframe_'); + + this.isYoutubeVideo = this.videoSrc.indexOf('youtube') >= 0; + this.isMobileEnv = config.device.size_class <= config.device.SIZES.LG && config.device.touch; + if (this.isYoutubeVideo && this.isMobileEnv) { + this.videoSrc = this.videoSrc + "&enablejsapi=1"; + + if (!window.YT) { + var oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady; + proms.push(new Promise(resolve => { + window.onYouTubeIframeAPIReady = () => { + if (oldOnYoutubeIframeAPIReady) { + oldOnYoutubeIframeAPIReady(); + } + return resolve(); + }; + })); + $('<script/>', { + src: 'https://www.youtube.com/iframe_api', + }).appendTo('head'); + } + } + + var throttledUpdate = _.throttle(() => this._adjustIframe(), 50); + + var $dropdownMenu = this.$el.closest('.dropdown-menu'); + if ($dropdownMenu.length) { + this.$dropdownParent = $dropdownMenu.parent(); + this.$dropdownParent.on('shown.bs.dropdown.backgroundVideo', throttledUpdate); + } + + $(window).on('resize.' + this.iframeID, throttledUpdate); + + return Promise.all(proms).then(() => this._appendBgVideo()); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + + if (this.$dropdownParent) { + this.$dropdownParent.off('.backgroundVideo'); + } + + $(window).off('resize.' + this.iframeID); + + if (this.$bgVideoContainer) { + this.$bgVideoContainer.remove(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Adjusts iframe sizes and position so that it fills the container and so + * that it is centered in it. + * + * @private + */ + _adjustIframe: function () { + if (!this.$iframe) { + return; + } + + this.$iframe.removeClass('show'); + + // Adjust the iframe + var wrapperWidth = this.$target.innerWidth(); + var wrapperHeight = this.$target.innerHeight(); + var relativeRatio = (wrapperWidth / wrapperHeight) / (16 / 9); + var style = {}; + if (relativeRatio >= 1.0) { + style['width'] = '100%'; + style['height'] = (relativeRatio * 100) + '%'; + style['left'] = '0'; + style['top'] = (-(relativeRatio - 1.0) / 2 * 100) + '%'; + } else { + style['width'] = ((1 / relativeRatio) * 100) + '%'; + style['height'] = '100%'; + style['left'] = (-((1 / relativeRatio) - 1.0) / 2 * 100) + '%'; + style['top'] = '0'; + } + this.$iframe.css(style); + + void this.$iframe[0].offsetWidth; // Force style addition + this.$iframe.addClass('show'); + }, + /** + * Append background video related elements to the target. + * + * @private + */ + _appendBgVideo: function () { + var $oldContainer = this.$bgVideoContainer || this.$('> .o_bg_video_container'); + this.$bgVideoContainer = $(qweb.render('website.background.video', { + videoSrc: this.videoSrc, + iframeID: this.iframeID, + })); + this.$iframe = this.$bgVideoContainer.find('.o_bg_video_iframe'); + this.$iframe.one('load', () => { + this.$bgVideoContainer.find('.o_bg_video_loading').remove(); + }); + this.$bgVideoContainer.prependTo(this.$target); + $oldContainer.remove(); + + this._adjustIframe(); + + // YouTube does not allow to auto-play video in mobile devices, so we + // have to play the video manually. + if (this.isMobileEnv && this.isYoutubeVideo) { + new window.YT.Player(this.iframeID, { + events: { + onReady: ev => ev.target.playVideo(), + } + }); + } + }, +}); + +registry.socialShare = publicWidget.Widget.extend({ + selector: '.oe_social_share', + xmlDependencies: ['/website/static/src/xml/website.share.xml'], + events: { + 'mouseenter': '_onMouseEnter', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _bindSocialEvent: function () { + this.$('.oe_social_facebook').click($.proxy(this._renderSocial, this, 'facebook')); + this.$('.oe_social_twitter').click($.proxy(this._renderSocial, this, 'twitter')); + this.$('.oe_social_linkedin').click($.proxy(this._renderSocial, this, 'linkedin')); + }, + /** + * @private + */ + _render: function () { + this.$el.popover({ + content: qweb.render('website.social_hover', {medias: this.socialList}), + placement: 'bottom', + container: this.$el, + html: true, + trigger: 'manual', + animation: false, + }).popover("show"); + + this.$el.off('mouseleave.socialShare').on('mouseleave.socialShare', function () { + var self = this; + setTimeout(function () { + if (!$(".popover:hover").length) { + $(self).popover('dispose'); + } + }, 200); + }); + }, + /** + * @private + */ + _renderSocial: function (social) { + var url = this.$el.data('urlshare') || document.URL.split(/[?#]/)[0]; + url = encodeURIComponent(url); + var title = document.title.split(" | ")[0]; // get the page title without the company name + var hashtags = ' #' + document.title.split(" | ")[1].replace(' ', '') + ' ' + this.hashtags; // company name without spaces (for hashtag) + var socialNetworks = { + 'facebook': 'https://www.facebook.com/sharer/sharer.php?u=' + url, + 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ') + url, + 'linkedin': 'https://www.linkedin.com/sharing/share-offsite/?url=' + url, + }; + if (!_.contains(_.keys(socialNetworks), social)) { + return; + } + var wHeight = 500; + var wWidth = 500; + window.open(socialNetworks[social], '', 'menubar=no, toolbar=no, resizable=yes, scrollbar=yes, height=' + wHeight + ',width=' + wWidth); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the user hovers the animation element -> open the social + * links popover. + * + * @private + */ + _onMouseEnter: function () { + var social = this.$el.data('social'); + this.socialList = social ? social.split(',') : ['facebook', 'twitter', 'linkedin']; + this.hashtags = this.$el.data('hashtags') || ''; + + this._render(); + this._bindSocialEvent(); + }, +}); + +registry.anchorSlide = publicWidget.Widget.extend({ + selector: 'a[href^="/"][href*="#"], a[href^="#"]', + events: { + 'click': '_onAnimateClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {jQuery} $el the element to scroll to. + * @param {string} [scrollValue='true'] scroll value + * @returns {Promise} + */ + async _scrollTo($el, scrollValue = 'true') { + return dom.scrollTo($el[0], { + duration: scrollValue === 'true' ? 500 : 0, + extraOffset: this._computeExtraOffset(), + }); + }, + /** + * @private + */ + _computeExtraOffset() { + return 0; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAnimateClick: function (ev) { + if (this.$target[0].pathname !== window.location.pathname) { + return; + } + var hash = this.$target[0].hash; + if (!utils.isValidAnchor(hash)) { + return; + } + var $anchor = $(hash); + const scrollValue = $anchor.attr('data-anchor'); + if (!$anchor.length || !scrollValue) { + return; + } + ev.preventDefault(); + this._scrollTo($anchor, scrollValue); + }, +}); + +registry.FullScreenHeight = publicWidget.Widget.extend({ + selector: '.o_full_screen_height', + disabledInEditableMode: false, + + /** + * @override + */ + start() { + if (this.$el.outerHeight() > this._computeIdealHeight()) { + // Only initialize if taller than the ideal height as some extra css + // rules may alter the full-screen-height class behavior in some + // cases (blog...). + this._adaptSize(); + $(window).on('resize.FullScreenHeight', _.debounce(() => this._adaptSize(), 250)); + } + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + $(window).off('.FullScreenHeight'); + this.el.style.setProperty('min-height', ''); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptSize() { + const height = this._computeIdealHeight(); + this.el.style.setProperty('min-height', `${height}px`, 'important'); + }, + /** + * @private + */ + _computeIdealHeight() { + const windowHeight = $(window).outerHeight(); + // Doing it that way allows to considerer fixed headers, hidden headers, + // connected users, ... + const firstContentEl = $('#wrapwrap > main > :first-child')[0]; // first child to consider the padding-top of main + const mainTopPos = firstContentEl.getBoundingClientRect().top + dom.closestScrollable(firstContentEl.parentNode).scrollTop; + return (windowHeight - mainTopPos); + }, +}); + +registry.ScrollButton = registry.anchorSlide.extend({ + selector: '.o_scroll_button', + + /** + * @override + */ + _onAnimateClick: function (ev) { + ev.preventDefault(); + const $nextElement = this.$el.closest('section').next(); + if ($nextElement.length) { + this._scrollTo($nextElement); + } + }, +}); + +registry.FooterSlideout = publicWidget.Widget.extend({ + selector: '#wrapwrap:has(.o_footer_slideout)', + disabledInEditableMode: false, + + /** + * @override + */ + async start() { + const $main = this.$('> main'); + const slideoutEffect = $main.outerHeight() >= $(window).outerHeight(); + this.el.classList.toggle('o_footer_effect_enable', slideoutEffect); + + // Add a pixel div over the footer, after in the DOM, so that the + // height of the footer is understood by Firefox sticky implementation + // (which it seems to not understand because of the combination of 3 + // items: the footer is the last :visible element in the #wrapwrap, the + // #wrapwrap uses flex layout and the #wrapwrap is the element with a + // scrollbar). + // TODO check if the hack is still needed by future browsers. + this.__pixelEl = document.createElement('div'); + this.__pixelEl.style.width = `1px`; + this.__pixelEl.style.height = `1px`; + this.__pixelEl.style.marginTop = `-1px`; + this.el.appendChild(this.__pixelEl); + + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + this.el.classList.remove('o_footer_effect_enable'); + this.__pixelEl.remove(); + }, +}); + +registry.HeaderHamburgerFull = publicWidget.Widget.extend({ + selector: 'header:has(.o_header_hamburger_full_toggler):not(:has(.o_offcanvas_menu_toggler))', + events: { + 'click .o_header_hamburger_full_toggler': '_onToggleClick', + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onToggleClick() { + document.body.classList.add('overflow-hidden'); + setTimeout(() => $(window).trigger('scroll'), 100); + }, +}); + +registry.BottomFixedElement = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + /** + * @override + */ + async start() { + this.$scrollingElement = $().getScrollingElement(); + this.__hideBottomFixedElements = _.debounce(() => this._hideBottomFixedElements(), 500); + this.$scrollingElement.on('scroll.bottom_fixed_element', this.__hideBottomFixedElements); + $(window).on('resize.bottom_fixed_element', this.__hideBottomFixedElements); + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + this.$scrollingElement.off('.bottom_fixed_element'); + $(window).off('.bottom_fixed_element'); + $('.o_bottom_fixed_element').removeClass('o_bottom_fixed_element_hidden'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Hides the elements that are fixed at the bottom of the screen if the + * scroll reaches the bottom of the page and if the elements hide a button. + * + * @private + */ + _hideBottomFixedElements() { + // Note: check in the whole DOM instead of #wrapwrap as unfortunately + // some things are still put outside of the #wrapwrap (like the livechat + // button which is the main reason of this code). + const $bottomFixedElements = $('.o_bottom_fixed_element'); + if (!$bottomFixedElements.length) { + return; + } + + $bottomFixedElements.removeClass('o_bottom_fixed_element_hidden'); + if ((this.$scrollingElement[0].offsetHeight + this.$scrollingElement[0].scrollTop) >= (this.$scrollingElement[0].scrollHeight - 2)) { + const buttonEls = [...this.$('.btn:visible')]; + for (const el of $bottomFixedElements) { + if (buttonEls.some(button => dom.areColliding(button, el))) { + el.classList.add('o_bottom_fixed_element_hidden'); + } + } + } + }, +}); + +return { + Widget: publicWidget.Widget, + Animation: Animation, + registry: registry, + + Class: Animation, // Deprecated +}; +}); diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js new file mode 100644 index 00000000..c2844a49 --- /dev/null +++ b/addons/website/static/src/js/content/website_root.js @@ -0,0 +1,350 @@ +odoo.define('website.root', function (require) { +'use strict'; + +const ajax = require('web.ajax'); +const {_t} = require('web.core'); +var Dialog = require('web.Dialog'); +const KeyboardNavigationMixin = require('web.KeyboardNavigationMixin'); +const session = require('web.session'); +var publicRootData = require('web.public.root'); +require("web.zoomodoo"); + +var websiteRootRegistry = publicRootData.publicRootRegistry; + +var WebsiteRoot = publicRootData.PublicRoot.extend(KeyboardNavigationMixin, { + events: _.extend({}, KeyboardNavigationMixin.events, publicRootData.PublicRoot.prototype.events || {}, { + 'click .js_change_lang': '_onLangChangeClick', + 'click .js_publish_management .js_publish_btn': '_onPublishBtnClick', + 'click .js_multi_website_switch': '_onWebsiteSwitch', + 'shown.bs.modal': '_onModalShown', + }), + custom_events: _.extend({}, publicRootData.PublicRoot.prototype.custom_events || {}, { + 'gmap_api_request': '_onGMapAPIRequest', + 'gmap_api_key_request': '_onGMapAPIKeyRequest', + 'ready_to_clean_for_save': '_onWidgetsStopRequest', + 'seo_object_request': '_onSeoObjectRequest', + }), + + /** + * @override + */ + init() { + this.isFullscreen = false; + KeyboardNavigationMixin.init.call(this, { + autoAccessKeys: false, + }); + return this._super(...arguments); + }, + /** + * @override + */ + start: function () { + KeyboardNavigationMixin.start.call(this); + // Compatibility lang change ? + if (!this.$('.js_change_lang').length) { + var $links = this.$('.js_language_selector a:not([data-oe-id])'); + var m = $(_.min($links, function (l) { + return $(l).attr('href').length; + })).attr('href'); + $links.each(function () { + var $link = $(this); + var t = $link.attr('href'); + var l = (t === m) ? "default" : t.split('/')[1]; + $link.data('lang', l).addClass('js_change_lang'); + }); + } + + // Enable magnify on zommable img + this.$('.zoomable img[data-zoom]').zoomOdoo(); + + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy() { + KeyboardNavigationMixin.destroy.call(this); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _getContext: function (context) { + var html = document.documentElement; + return _.extend({ + 'website_id': html.getAttribute('data-website-id') | 0, + }, this._super.apply(this, arguments)); + }, + /** + * @override + */ + _getExtraContext: function (context) { + var html = document.documentElement; + return _.extend({ + 'editable': !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python + 'translatable': !!html.dataset.translatable, + 'edit_translations': !!html.dataset.edit_translations, + }, this._super.apply(this, arguments)); + }, + /** + * @private + * @param {boolean} [refetch=false] + */ + async _getGMapAPIKey(refetch) { + if (refetch || !this._gmapAPIKeyProm) { + this._gmapAPIKeyProm = new Promise(async resolve => { + const data = await this._rpc({ + route: '/website/google_maps_api_key', + }); + resolve(JSON.parse(data).google_maps_api_key || ''); + }); + } + return this._gmapAPIKeyProm; + }, + /** + * @override + */ + _getPublicWidgetsRegistry: function (options) { + var registry = this._super.apply(this, arguments); + if (options.editableMode) { + return _.pick(registry, function (PublicWidget) { + return !PublicWidget.prototype.disabledInEditableMode; + }); + } + return registry; + }, + /** + * @private + * @param {boolean} [editableMode=false] + * @param {boolean} [refetch=false] + */ + async _loadGMapAPI(editableMode, refetch) { + // Note: only need refetch to reload a configured key and load the + // library. If the library was loaded with a correct key and that the + // key changes meanwhile... it will not work but we can agree the user + // can bother to reload the page at that moment. + if (refetch || !this._gmapAPILoading) { + this._gmapAPILoading = new Promise(async resolve => { + const key = await this._getGMapAPIKey(refetch); + + window.odoo_gmap_api_post_load = (async function odoo_gmap_api_post_load() { + await this._startWidgets(undefined, {editableMode: editableMode}); + resolve(key); + }).bind(this); + + if (!key) { + if (!editableMode && session.is_admin) { + this.displayNotification({ + type: 'warning', + sticky: true, + message: + $('<div/>').append( + $('<span/>', {text: _t("Cannot load google map.")}), + $('<br/>'), + $('<a/>', { + href: "/web#action=website.action_website_configuration", + text: _t("Check your configuration."), + }), + )[0].outerHTML, + }); + } + resolve(false); + this._gmapAPILoading = false; + return; + } + await ajax.loadJS(`https://maps.googleapis.com/maps/api/js?v=3.exp&libraries=places&callback=odoo_gmap_api_post_load&key=${key}`); + }); + } + return this._gmapAPILoading; + }, + /** + * Toggles the fullscreen mode. + * + * @private + * @param {boolean} state toggle fullscreen on/off (true/false) + */ + _toggleFullscreen(state) { + this.isFullscreen = state; + document.body.classList.add('o_fullscreen_transition'); + document.body.classList.toggle('o_fullscreen', this.isFullscreen); + document.body.style.overflowX = 'hidden'; + let resizing = true; + window.requestAnimationFrame(function resizeFunction() { + window.dispatchEvent(new Event('resize')); + if (resizing) { + window.requestAnimationFrame(resizeFunction); + } + }); + let stopResizing; + const onTransitionEnd = ev => { + if (ev.target === document.body && ev.propertyName === 'padding-top') { + stopResizing(); + } + }; + stopResizing = () => { + resizing = false; + document.body.style.overflowX = ''; + document.body.removeEventListener('transitionend', onTransitionEnd); + document.body.classList.remove('o_fullscreen_transition'); + }; + document.body.addEventListener('transitionend', onTransitionEnd); + // Safeguard in case the transitionend event doesn't trigger for whatever reason. + window.setTimeout(() => stopResizing(), 500); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _onWidgetsStartRequest: function (ev) { + ev.data.options = _.clone(ev.data.options || {}); + ev.data.options.editableMode = ev.data.editableMode; + this._super.apply(this, arguments); + }, + /** + * @todo review + * @private + */ + _onLangChangeClick: function (ev) { + ev.preventDefault(); + + var $target = $(ev.currentTarget); + // retrieve the hash before the redirect + var redirect = { + lang: $target.data('url_code'), + url: encodeURIComponent($target.attr('href').replace(/[&?]edit_translations[^&?]+/, '')), + hash: encodeURIComponent(window.location.hash) + }; + window.location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect); + }, + /** + * @private + * @param {OdooEvent} ev + */ + async _onGMapAPIRequest(ev) { + ev.stopPropagation(); + const apiKey = await this._loadGMapAPI(ev.data.editableMode, ev.data.refetch); + ev.data.onSuccess(apiKey); + }, + /** + * @private + * @param {OdooEvent} ev + */ + async _onGMapAPIKeyRequest(ev) { + ev.stopPropagation(); + const apiKey = await this._getGMapAPIKey(ev.data.refetch); + ev.data.onSuccess(apiKey); + }, + /** + /** + * Checks information about the page SEO object. + * + * @private + * @param {OdooEvent} ev + */ + _onSeoObjectRequest: function (ev) { + var res = this._unslugHtmlDataObject('seo-object'); + ev.data.callback(res); + }, + /** + * Returns a model/id object constructed from html data attribute. + * + * @private + * @param {string} dataAttr + * @returns {Object} an object with 2 keys: model and id, or null + * if not found + */ + _unslugHtmlDataObject: function (dataAttr) { + var repr = $('html').data(dataAttr); + var match = repr && repr.match(/(.+)\((\d+),(.*)\)/); + if (!match) { + return null; + } + return { + model: match[1], + id: match[2] | 0, + }; + }, + /** + * @todo review + * @private + */ + _onPublishBtnClick: function (ev) { + ev.preventDefault(); + if (document.body.classList.contains('editor_enable')) { + return; + } + + var self = this; + var $data = $(ev.currentTarget).parents(".js_publish_management:first"); + this._rpc({ + route: $data.data('controller') || '/website/publish', + params: { + id: +$data.data('id'), + object: $data.data('object'), + }, + }) + .then(function (result) { + $data.toggleClass("css_unpublished css_published"); + $data.find('input').prop("checked", result); + $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off'); + if (result) { + self.displayNotification({ + type: 'success', + message: $data.data('description') ? + _.str.sprintf(_t("You've published your %s."), $data.data('description')) : + _t("Published with success."), + }); + } + }); + }, + /** + * @private + * @param {Event} ev + */ + _onWebsiteSwitch: function (ev) { + var websiteId = ev.currentTarget.getAttribute('website-id'); + var websiteDomain = ev.currentTarget.getAttribute('domain'); + let url = `/website/force/${websiteId}`; + if (websiteDomain && window.location.hostname !== websiteDomain) { + url = websiteDomain + url; + } + const path = window.location.pathname + window.location.search + window.location.hash; + window.location.href = $.param.querystring(url, {'path': path}); + }, + /** + * @private + * @param {Event} ev + */ + _onModalShown: function (ev) { + $(ev.target).addClass('modal_shown'); + }, + /** + * @override + */ + _onKeyDown(ev) { + if (!session.user_id) { + return; + } + // If document.body doesn't contain the element, it was probably removed as a consequence of pressing Esc. + // we don't want to toggle fullscreen as the removal (eg, closing a modal) is the intended action. + if (ev.keyCode !== $.ui.keyCode.ESCAPE || !document.body.contains(ev.target) || ev.target.closest('.modal')) { + return KeyboardNavigationMixin._onKeyDown.apply(this, arguments); + } + this._toggleFullscreen(!this.isFullscreen); + }, +}); + +return { + WebsiteRoot: WebsiteRoot, + websiteRootRegistry: websiteRootRegistry, +}; +}); diff --git a/addons/website/static/src/js/content/website_root_instance.js b/addons/website/static/src/js/content/website_root_instance.js new file mode 100644 index 00000000..fbbefb03 --- /dev/null +++ b/addons/website/static/src/js/content/website_root_instance.js @@ -0,0 +1,26 @@ +odoo.define('root.widget', function (require) { +'use strict'; + +const AbstractService = require('web.AbstractService'); +const env = require('web.public_env'); +var lazyloader = require('web.public.lazyloader'); +var websiteRootData = require('website.root'); + +/** + * Configure Owl with the public env + */ +owl.config.mode = env.isDebug() ? "dev" : "prod"; +owl.Component.env = env; + +/** + * Deploy services in the env + */ +AbstractService.prototype.deployServices(env); + +var websiteRoot = new websiteRootData.WebsiteRoot(null); +return lazyloader.allScriptsLoaded.then(function () { + return websiteRoot.attachTo(document.body).then(function () { + return websiteRoot; + }); +}); +}); diff --git a/addons/website/static/src/js/editor/editor.js b/addons/website/static/src/js/editor/editor.js new file mode 100644 index 00000000..ca26c092 --- /dev/null +++ b/addons/website/static/src/js/editor/editor.js @@ -0,0 +1,18 @@ +odoo.define('website.editor', function (require) { +'use strict'; + +var weWidgets = require('web_editor.widget'); +var wUtils = require('website.utils'); + +weWidgets.LinkDialog.include({ + /** + * Allows the URL input to propose existing website pages. + * + * @override + */ + start: function () { + wUtils.autocompleteWithPages(this, this.$('input[name="url"]')); + return this._super.apply(this, arguments); + }, +}); +}); diff --git a/addons/website/static/src/js/editor/editor_menu.js b/addons/website/static/src/js/editor/editor_menu.js new file mode 100644 index 00000000..e330df97 --- /dev/null +++ b/addons/website/static/src/js/editor/editor_menu.js @@ -0,0 +1,256 @@ +odoo.define('website.editor.menu', function (require) { +'use strict'; + +var Dialog = require('web.Dialog'); +var dom = require('web.dom'); +var Widget = require('web.Widget'); +var core = require('web.core'); +var Wysiwyg = require('web_editor.wysiwyg.root'); + +var _t = core._t; + +var WysiwygMultizone = Wysiwyg.extend({ + assetLibs: Wysiwyg.prototype.assetLibs.concat(['website.compiled_assets_wysiwyg']), + _getWysiwygContructor: function () { + return odoo.__DEBUG__.services['web_editor.wysiwyg.multizone']; + } +}); + +var EditorMenu = Widget.extend({ + template: 'website.editorbar', + xmlDependencies: ['/website/static/src/xml/website.editor.xml'], + events: { + 'click button[data-action=undo]': '_onUndoClick', + 'click button[data-action=redo]': '_onRedoClick', + 'click button[data-action=save]': '_onSaveClick', + 'click button[data-action=cancel]': '_onCancelClick', + }, + custom_events: { + request_save: '_onSnippetRequestSave', + get_clean_html: '_onGetCleanHTML', + }, + + /** + * @override + */ + willStart: function () { + var self = this; + this.$el = null; // temporary null to avoid hidden error (@see start) + return this._super() + .then(function () { + var $wrapwrap = $('#wrapwrap'); + $wrapwrap.removeClass('o_editable'); // clean the dom before edition + self.editable($wrapwrap).addClass('o_editable'); + self.wysiwyg = self._wysiwygInstance(); + }); + }, + /** + * @override + */ + start: function () { + var self = this; + this.$el.css({width: '100%'}); + return this.wysiwyg.attachTo($('#wrapwrap')).then(function () { + self.trigger_up('edit_mode'); + self.$el.css({width: ''}); + }); + }, + /** + * @override + */ + destroy: function () { + this.trigger_up('readonly_mode'); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Asks the user if they really wants to discard their changes (if any), + * then simply reloads the page if they want to. + * + * @param {boolean} [reload=true] + * true if the page has to be reloaded when the user answers yes + * (do nothing otherwise but add this to allow class extension) + * @returns {Deferred} + */ + cancel: function (reload) { + var self = this; + var def = new Promise(function (resolve, reject) { + if (!self.wysiwyg.isDirty()) { + resolve(); + } else { + var confirm = Dialog.confirm(self, _t("If you discard the current edits, all unsaved changes will be lost. You can cancel to return to edit mode."), { + confirm_callback: resolve, + }); + confirm.on('closed', self, reject); + } + }); + + return def.then(function () { + self.trigger_up('edition_will_stopped'); + var $wrapwrap = $('#wrapwrap'); + self.editable($wrapwrap).removeClass('o_editable'); + if (reload !== false) { + window.onbeforeunload = null; + self.wysiwyg.destroy(); + return self._reload(); + } else { + self.wysiwyg.destroy(); + self.trigger_up('readonly_mode'); + self.trigger_up('edition_was_stopped'); + self.destroy(); + } + }); + }, + /** + * Asks the snippets to clean themself, then saves the page, then reloads it + * if asked to. + * + * @param {boolean} [reload=true] + * true if the page has to be reloaded after the save + * @returns {Promise} + */ + save: async function (reload) { + if (this._saving) { + return false; + } + var self = this; + this._saving = true; + this.trigger_up('edition_will_stopped'); + return this.wysiwyg.save(false).then(function (result) { + var $wrapwrap = $('#wrapwrap'); + self.editable($wrapwrap).removeClass('o_editable'); + if (result.isDirty && reload !== false) { + // remove top padding because the connected bar is not visible + $('body').removeClass('o_connected_user'); + return self._reload(); + } else { + self.wysiwyg.destroy(); + self.trigger_up('edition_was_stopped'); + self.destroy(); + } + return true; + }).guardedCatch(() => { + this._saving = false; + }); + }, + /** + * Returns the editable areas on the page. + * + * @param {DOM} $wrapwrap + * @returns {jQuery} + */ + editable: function ($wrapwrap) { + return $wrapwrap.find('[data-oe-model]') + .not('.o_not_editable') + .filter(function () { + var $parent = $(this).closest('.o_editable, .o_not_editable'); + return !$parent.length || $parent.hasClass('o_editable'); + }) + .not('link, script') + .not('[data-oe-readonly]') + .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]') + .not('.oe_snippet_editor') + .not('hr, br, input, textarea') + .add('.o_editable'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _wysiwygInstance: function () { + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + return new WysiwygMultizone(this, { + snippets: 'website.snippets', + recordInfo: { + context: context, + data_res_model: 'website', + data_res_id: context.website_id, + } + }); + }, + /** + * Reloads the page in non-editable mode, with the right scrolling. + * + * @private + * @returns {Deferred} (never resolved, the page is reloading anyway) + */ + _reload: function () { + $('body').addClass('o_wait_reload'); + this.wysiwyg.destroy(); + this.$el.hide(); + window.location.hash = 'scrollTop=' + window.document.body.scrollTop; + window.location.reload(true); + return new Promise(function () {}); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the "Discard" button is clicked -> discards the changes. + * + * @private + */ + _onCancelClick: function () { + this.cancel(true); + }, + /** + * Get the cleaned value of the editable element. + * + * @private + * @param {OdooEvent} ev + */ + _onGetCleanHTML: function (ev) { + ev.data.callback(this.wysiwyg.getValue({$layout: ev.data.$layout})); + }, + /** + * Snippet (menu_data) can request to save the document to leave the page + * + * @private + * @param {OdooEvent} ev + * @param {object} ev.data + * @param {function} ev.data.onSuccess + * @param {function} ev.data.onFailure + */ + _onSnippetRequestSave: function (ev) { + this.save(false).then(ev.data.onSuccess, ev.data.onFailure); + }, + /** + * Called when the "Save" button is clicked -> saves the changes. + * + * @private + */ + _onSaveClick: function (ev) { + const restore = dom.addButtonLoadingEffect(ev.currentTarget); + this.save().then(restore).guardedCatch(restore); + }, + /** + * @private + */ + _onUndoClick() { + $('.note-history [data-event=undo]').first().click(); + }, + /** + * @private + */ + _onRedoClick() { + $('.note-history [data-event=redo]').first().click(); + }, +}); + +return EditorMenu; +}); diff --git a/addons/website/static/src/js/editor/editor_menu_translate.js b/addons/website/static/src/js/editor/editor_menu_translate.js new file mode 100644 index 00000000..3016ade6 --- /dev/null +++ b/addons/website/static/src/js/editor/editor_menu_translate.js @@ -0,0 +1,109 @@ +odoo.define('website.editor.menu.translate', function (require) { +'use strict'; + +require('web.dom_ready'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var localStorage = require('web.local_storage'); +var Wysiwyg = require('web_editor.wysiwyg.root'); +var EditorMenu = require('website.editor.menu'); + +var _t = core._t; + +var localStorageNoDialogKey = 'website_translator_nodialog'; + +var TranslatorInfoDialog = Dialog.extend({ + template: 'website.TranslatorInfoDialog', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/translator.xml'] + ), + + /** + * @constructor + */ + init: function (parent, options) { + this._super(parent, _.extend({ + title: _t("Translation Info"), + buttons: [ + {text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)}, + {text: _t("Ok"), close: true} + ], + }, options || {})); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the "strong" ok is clicked -> adapt localstorage to make sure + * the dialog is never displayed again. + * + * @private + */ + _onStrongOk: function () { + localStorage.setItem(localStorageNoDialogKey, true); + }, +}); + +var WysiwygTranslate = Wysiwyg.extend({ + assetLibs: Wysiwyg.prototype.assetLibs.concat(['website.compiled_assets_wysiwyg']), + _getWysiwygContructor: function () { + return odoo.__DEBUG__.services['web_editor.wysiwyg.multizone.translate']; + } +}); + +var TranslatorMenu = EditorMenu.extend({ + + /** + * @override + */ + start: function () { + if (!localStorage.getItem(localStorageNoDialogKey)) { + new TranslatorInfoDialog(this).open(); + } + + return this._super(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the editable areas on the page. + * + * @param {DOM} $wrapwrap + * @returns {jQuery} + */ + editable: function ($wrapwrap) { + var selector = '[data-oe-translation-id], '+ + '[data-oe-model][data-oe-id][data-oe-field], ' + + '[placeholder*="data-oe-translation-id="], ' + + '[title*="data-oe-translation-id="], ' + + '[alt*="data-oe-translation-id="]'; + var $edit = $wrapwrap.find(selector); + $edit.filter(':has(' + selector + ')').attr('data-oe-readonly', true); + return $edit.not('[data-oe-readonly]'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _wysiwygInstance: function () { + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + return new WysiwygTranslate(this, {lang: context.lang}); + }, +}); + +return TranslatorMenu; +}); diff --git a/addons/website/static/src/js/editor/mega_menu.js b/addons/website/static/src/js/editor/mega_menu.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/addons/website/static/src/js/editor/mega_menu.js diff --git a/addons/website/static/src/js/editor/rte.summernote.js b/addons/website/static/src/js/editor/rte.summernote.js new file mode 100644 index 00000000..36cadca7 --- /dev/null +++ b/addons/website/static/src/js/editor/rte.summernote.js @@ -0,0 +1,59 @@ +odoo.define('website.rte.summernote', function (require) { +'use strict'; + +var core = require('web.core'); +require('web_editor.rte.summernote'); + +var eventHandler = $.summernote.eventHandler; +var renderer = $.summernote.renderer; +var tplIconButton = renderer.getTemplate().iconButton; +var _t = core._t; + +var fn_tplPopovers = renderer.tplPopovers; +renderer.tplPopovers = function (lang, options) { + var $popover = $(fn_tplPopovers.call(this, lang, options)); + $popover.find('.note-image-popover .btn-group:has([data-value="img-thumbnail"])').append( + tplIconButton('fa fa-object-ungroup', { + title: _t('Transform the picture (click twice to reset transformation)'), + event: 'transform', + })); + return $popover; +}; + +$.summernote.pluginEvents.transform = function (event, editor, layoutInfo, sorted) { + var $selection = layoutInfo.handle().find('.note-control-selection'); + var $image = $($selection.data('target')); + + if ($image.data('transfo-destroy')) { + $image.removeData('transfo-destroy'); + return; + } + + $image.transfo(); + + var mouseup = function (event) { + $('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]')); + }; + $(document).on('mouseup', mouseup); + + var mousedown = function (event) { + if (!$(event.target).closest('.transfo-container').length) { + $image.transfo('destroy'); + $(document).off('mousedown', mousedown).off('mouseup', mouseup); + } + if ($(event.target).closest('.note-popover').length) { + $image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(/[^;]*transform[\w:]*;?/g, '')); + } + $image.trigger('content_changed'); + }; + $(document).on('mousedown', mousedown); +}; + +var fn_boutton_update = eventHandler.modules.popover.button.update; +eventHandler.modules.popover.button.update = function ($container, oStyle) { + fn_boutton_update.call(this, $container, oStyle); + $container.find('button[data-event="transform"]') + .toggleClass('active', $(oStyle.image).is('[style*="transform"]')) + .toggleClass('d-none', !$(oStyle.image).is('img')); +}; +}); diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js new file mode 100644 index 00000000..15f80046 --- /dev/null +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -0,0 +1,245 @@ +odoo.define('website.snippet.editor', function (require) { +'use strict'; + +const {qweb, _t, _lt} = require('web.core'); +const Dialog = require('web.Dialog'); +const weSnippetEditor = require('web_editor.snippet.editor'); +const wSnippetOptions = require('website.editor.snippets.options'); + +const FontFamilyPickerUserValueWidget = wSnippetOptions.FontFamilyPickerUserValueWidget; + +weSnippetEditor.Class.include({ + xmlDependencies: (weSnippetEditor.Class.prototype.xmlDependencies || []) + .concat(['/website/static/src/xml/website.editor.xml']), + events: _.extend({}, weSnippetEditor.Class.prototype.events, { + 'click .o_we_customize_theme_btn': '_onThemeTabClick', + }), + custom_events: Object.assign({}, weSnippetEditor.Class.prototype.custom_events, { + 'gmap_api_request': '_onGMapAPIRequest', + 'gmap_api_key_request': '_onGMapAPIKeyRequest', + }), + tabs: _.extend({}, weSnippetEditor.Class.prototype.tabs, { + THEME: 'theme', + }), + optionsTabStructure: [ + ['theme-colors', _lt("Theme Colors")], + ['theme-options', _lt("Theme Options")], + ['website-settings', _lt("Website Settings")], + ], + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeSnippetTemplates: function (html) { + const $html = $(html); + const fontVariables = _.map($html.find('we-fontfamilypicker[data-variable]'), el => { + return el.dataset.variable; + }); + FontFamilyPickerUserValueWidget.prototype.fontVariables = fontVariables; + + return this._super(...arguments); + }, + /** + * Depending of the demand, reconfigure they gmap key or configure it + * if not already defined. + * + * @private + * @param {boolean} [reconfigure=false] + * @param {boolean} [onlyIfUndefined=false] + */ + async _configureGMapAPI({reconfigure, onlyIfUndefined}) { + const apiKey = await new Promise(resolve => { + this.getParent().trigger_up('gmap_api_key_request', { + onSuccess: key => resolve(key), + }); + }); + if (!reconfigure && (apiKey || !onlyIfUndefined)) { + return false; + } + let websiteId; + this.trigger_up('context_get', { + callback: ctx => websiteId = ctx['website_id'], + }); + return new Promise(resolve => { + let invalidated = false; + const dialog = new Dialog(this, { + size: 'medium', + title: _t("Google Map API Key"), + buttons: [ + {text: _t("Save"), classes: 'btn-primary', click: async (ev) => { + const $apiKeyInput = dialog.$('#api_key_input'); + const valueAPIKey = $apiKeyInput.val(); + const $apiKeyHelp = dialog.$('#api_key_help'); + if (!valueAPIKey) { + $apiKeyInput.addClass('is-invalid'); + $apiKeyHelp.text(_t("Enter an API Key")); + return; + } + const $button = $(ev.currentTarget); + $button.prop('disabled', true); + try { + const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${valueAPIKey}`); + if (response.status === 200) { + await this._rpc({ + model: 'website', + method: 'write', + args: [ + [websiteId], + {google_maps_api_key: valueAPIKey}, + ], + }); + invalidated = true; + dialog.close(); + } else { + const text = await response.text(); + $apiKeyInput.addClass('is-invalid'); + $apiKeyHelp.empty().text( + _t("Invalid API Key. The following error was returned by Google:") + ).append($('<i/>', { + text: text, + class: 'ml-1', + })); + } + } catch (e) { + $apiKeyHelp.text(_t("Check your connection and try again")); + } finally { + $button.prop("disabled", false); + } + }}, + {text: _t("Cancel"), close: true} + ], + $content: $(qweb.render('website.s_google_map_modal', { + apiKey: apiKey, + })), + }); + dialog.on('closed', this, () => resolve(invalidated)); + dialog.open(); + }); + }, + /** + * @override + */ + _getScrollOptions(options = {}) { + const finalOptions = this._super(...arguments); + if (!options.offsetElements || !options.offsetElements.$top) { + const $header = $('#top'); + if ($header.length) { + finalOptions.offsetElements = finalOptions.offsetElements || {}; + finalOptions.offsetElements.$top = $header; + } + } + return finalOptions; + }, + /** + * @private + * @param {OdooEvent} ev + * @param {string} gmapRequestEventName + */ + async _handleGMapRequest(ev, gmapRequestEventName) { + ev.stopPropagation(); + const reconfigured = await this._configureGMapAPI({ + reconfigure: ev.data.reconfigure, + onlyIfUndefined: ev.data.configureIfNecessary, + }); + this.getParent().trigger_up(gmapRequestEventName, { + refetch: reconfigured, + editableMode: true, + onSuccess: key => ev.data.onSuccess(key), + }); + }, + /** + * @override + */ + _updateLeftPanelContent: function ({content, tab}) { + this._super(...arguments); + this.$('.o_we_customize_theme_btn').toggleClass('active', tab === this.tabs.THEME); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + */ + _onGMapAPIRequest(ev) { + this._handleGMapRequest(ev, 'gmap_api_request'); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onGMapAPIKeyRequest(ev) { + this._handleGMapRequest(ev, 'gmap_api_key_request'); + }, + /** + * @private + */ + async _onThemeTabClick(ev) { + // Note: nothing async here but start the loading effect asap + let releaseLoader; + try { + const promise = new Promise(resolve => releaseLoader = resolve); + this._execWithLoadingEffect(() => promise, false, 0); + // loader is added to the DOM synchronously + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + // ensure loader is rendered: first call asks for the (already done) DOM update, + // second call happens only after rendering the first "updates" + + if (!this.topFakeOptionEl) { + let el; + for (const [elementName, title] of this.optionsTabStructure) { + const newEl = document.createElement(elementName); + newEl.dataset.name = title; + if (el) { + el.appendChild(newEl); + } else { + this.topFakeOptionEl = newEl; + } + el = newEl; + } + this.bottomFakeOptionEl = el; + this.el.appendChild(this.topFakeOptionEl); + } + + // Need all of this in that order so that: + // - the element is visible and can be enabled and the onFocus method is + // called each time. + // - the element is hidden afterwards so it does not take space in the + // DOM, same as the overlay which may make a scrollbar appear. + this.topFakeOptionEl.classList.remove('d-none'); + const editorPromise = this._activateSnippet($(this.bottomFakeOptionEl)); + releaseLoader(); // because _activateSnippet uses the same mutex as the loader + releaseLoader = undefined; + const editor = await editorPromise; + this.topFakeOptionEl.classList.add('d-none'); + editor.toggleOverlay(false); + + this._updateLeftPanelContent({ + tab: this.tabs.THEME, + }); + } catch (e) { + // Normally the loading effect is removed in case of error during the action but here + // the actual activity is happening outside of the action, the effect must therefore + // be cleared in case of error as well + if (releaseLoader) { + releaseLoader(); + } + throw e; + } + }, +}); + +weSnippetEditor.Editor.include({ + layoutElementsSelector: [ + weSnippetEditor.Editor.prototype.layoutElementsSelector, + '.s_parallax_bg', + '.o_bg_video_container', + ].join(','), +}); +}); diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js new file mode 100644 index 00000000..d0b032a1 --- /dev/null +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -0,0 +1,2612 @@ +odoo.define('website.editor.snippets.options', function (require) { +'use strict'; + +const {ColorpickerWidget} = require('web.Colorpicker'); +const config = require('web.config'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +const dom = require('web.dom'); +const weUtils = require('web_editor.utils'); +var options = require('web_editor.snippets.options'); +const wUtils = require('website.utils'); +require('website.s_popup_options'); + +var _t = core._t; +var qweb = core.qweb; + +const InputUserValueWidget = options.userValueWidgetsRegistry['we-input']; +const SelectUserValueWidget = options.userValueWidgetsRegistry['we-select']; + +const UrlPickerUserValueWidget = InputUserValueWidget.extend({ + custom_events: _.extend({}, InputUserValueWidget.prototype.custom_events || {}, { + 'website_url_chosen': '_onWebsiteURLChosen', + }), + events: _.extend({}, InputUserValueWidget.prototype.events || {}, { + 'click .o_we_redirect_to': '_onRedirectTo', + }), + + /** + * @override + */ + start: async function () { + await this._super(...arguments); + const linkButton = document.createElement('we-button'); + const icon = document.createElement('i'); + icon.classList.add('fa', 'fa-fw', 'fa-external-link') + linkButton.classList.add('o_we_redirect_to'); + linkButton.title = _t("Redirect to URL in a new tab"); + linkButton.appendChild(icon); + this.containerEl.appendChild(linkButton); + this.el.classList.add('o_we_large_input'); + this.inputEl.classList.add('text-left'); + const options = { + position: { + collision: 'flip fit', + }, + classes: { + "ui-autocomplete": 'o_website_ui_autocomplete' + }, + } + wUtils.autocompleteWithPages(this, $(this.inputEl), options); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the autocomplete change the input value. + * + * @private + * @param {OdooEvent} ev + */ + _onWebsiteURLChosen: function (ev) { + this._value = this.inputEl.value; + this._onUserValueChange(ev); + }, + /** + * Redirects to the URL the widget currently holds. + * + * @private + */ + _onRedirectTo: function () { + if (this._value) { + window.open(this._value, '_blank'); + } + }, +}); + +const FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({ + xmlDependencies: (SelectUserValueWidget.prototype.xmlDependencies || []) + .concat(['/website/static/src/xml/website.editor.xml']), + events: _.extend({}, SelectUserValueWidget.prototype.events || {}, { + 'click .o_we_add_google_font_btn': '_onAddGoogleFontClick', + 'click .o_we_delete_google_font_btn': '_onDeleteGoogleFontClick', + }), + fontVariables: [], // Filled by editor menu when all options are loaded + + /** + * @override + */ + start: async function () { + const style = window.getComputedStyle(document.documentElement); + const nbFonts = parseInt(weUtils.getCSSVariableValue('number-of-fonts', style)); + const googleFontsProperty = weUtils.getCSSVariableValue('google-fonts', style); + this.googleFonts = googleFontsProperty ? googleFontsProperty.split(/\s*,\s*/g) : []; + this.googleFonts = this.googleFonts.map(font => font.substring(1, font.length - 1)); // Unquote + + await this._super(...arguments); + + const fontEls = []; + const methodName = this.el.dataset.methodName || 'customizeWebsiteVariable'; + const variable = this.el.dataset.variable; + _.times(nbFonts, fontNb => { + const realFontNb = fontNb + 1; + const fontEl = document.createElement('we-button'); + fontEl.classList.add(`o_we_option_font_${realFontNb}`); + fontEl.dataset.variable = variable; + fontEl.dataset[methodName] = weUtils.getCSSVariableValue(`font-number-${realFontNb}`, style); + fontEl.dataset.font = realFontNb; + fontEls.push(fontEl); + this.menuEl.appendChild(fontEl); + }); + + if (this.googleFonts.length) { + const googleFontsEls = fontEls.slice(-this.googleFonts.length); + googleFontsEls.forEach((el, index) => { + $(el).append(core.qweb.render('website.delete_google_font_btn', { + index: index, + })); + }); + } + $(this.menuEl).append($(core.qweb.render('website.add_google_font_btn', { + variable: variable, + }))); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + async setValue() { + await this._super(...arguments); + + for (const className of this.menuTogglerEl.classList) { + if (className.match(/^o_we_option_font_\d+$/)) { + this.menuTogglerEl.classList.remove(className); + } + } + const activeWidget = this._userValueWidgets.find(widget => !widget.isPreviewed() && widget.isActive()); + if (activeWidget) { + this.menuTogglerEl.classList.add(`o_we_option_font_${activeWidget.el.dataset.font}`); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAddGoogleFontClick: function (ev) { + const variable = $(ev.currentTarget).data('variable'); + const dialog = new Dialog(this, { + title: _t("Add a Google Font"), + $content: $(core.qweb.render('website.dialog.addGoogleFont')), + buttons: [ + { + text: _t("Save & Reload"), + classes: 'btn-primary', + click: async () => { + const inputEl = dialog.el.querySelector('.o_input_google_font'); + // if font page link (what is expected) + let m = inputEl.value.match(/\bspecimen\/([\w+]+)/); + if (!m) { + // if embed code (so that it works anyway if the user put the embed code instead of the page link) + m = inputEl.value.match(/\bfamily=([\w+]+)/); + if (!m) { + inputEl.classList.add('is-invalid'); + return; + } + } + + let isValidFamily = false; + + try { + const result = await fetch("https://fonts.googleapis.com/css?family=" + m[1], {method: 'HEAD'}); + // Google fonts server returns a 400 status code if family is not valid. + if (result.ok) { + isValidFamily = true; + } + } catch (error) { + console.error(error); + } + + if (!isValidFamily) { + inputEl.classList.add('is-invalid'); + return; + } + + const font = m[1].replace(/\+/g, ' '); + this.googleFonts.push(font); + this.trigger_up('google_fonts_custo_request', { + values: {[variable]: `'${font}'`}, + googleFonts: this.googleFonts, + }); + }, + }, + { + text: _t("Discard"), + close: true, + }, + ], + }); + dialog.open(); + }, + /** + * @private + * @param {Event} ev + */ + _onDeleteGoogleFontClick: async function (ev) { + ev.preventDefault(); + + const save = await new Promise(resolve => { + Dialog.confirm(this, _t("Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?"), { + confirm_callback: () => resolve(true), + cancel_callback: () => resolve(false), + }); + }); + if (!save) { + return; + } + + // Remove Google font + const googleFontIndex = parseInt(ev.target.dataset.fontIndex); + const googleFont = this.googleFonts[googleFontIndex]; + this.googleFonts.splice(googleFontIndex, 1); + + // Adapt font variable indexes to the removal + const values = {}; + const style = window.getComputedStyle(document.documentElement); + _.each(FontFamilyPickerUserValueWidget.prototype.fontVariables, variable => { + const value = weUtils.getCSSVariableValue(variable, style); + if (value.substring(1, value.length - 1) === googleFont) { + // If an element is using the google font being removed, reset + // it to the theme default. + values[variable] = 'null'; + } + }); + + this.trigger_up('google_fonts_custo_request', { + values: values, + googleFonts: this.googleFonts, + }); + }, +}); + +const GPSPicker = InputUserValueWidget.extend({ + events: { // Explicitely not consider all InputUserValueWidget events + 'blur input': '_onInputBlur', + }, + + /** + * @constructor + */ + init() { + this._super(...arguments); + this._gmapCacheGPSToPlace = {}; + }, + /** + * @override + */ + async willStart() { + await this._super(...arguments); + this._gmapLoaded = await new Promise(resolve => { + this.trigger_up('gmap_api_request', { + editableMode: true, + configureIfNecessary: true, + onSuccess: key => resolve(!!key), + }); + }); + if (!this._gmapLoaded) { + this.trigger_up('user_value_widget_critical'); + return; + } + }, + /** + * @override + */ + async start() { + await this._super(...arguments); + this.el.classList.add('o_we_large_input'); + if (!this._gmapLoaded) { + return; + } + + this._gmapAutocomplete = new google.maps.places.Autocomplete(this.inputEl, {types: ['geocode']}); + google.maps.event.addListener(this._gmapAutocomplete, 'place_changed', this._onPlaceChanged.bind(this)); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getMethodsParams: function (methodName) { + return Object.assign({gmapPlace: this._gmapPlace || {}}, this._super(...arguments)); + }, + /** + * @override + */ + async setValue() { + await this._super(...arguments); + + await new Promise(resolve => { + const gps = this._value; + if (this._gmapCacheGPSToPlace[gps]) { + this._gmapPlace = this._gmapCacheGPSToPlace[gps]; + resolve(); + return; + } + const service = new google.maps.places.PlacesService(document.createElement('div')); + const p = gps.substring(1).slice(0, -1).split(','); + const location = new google.maps.LatLng(p[0] || 0, p[1] || 0); + service.nearbySearch({ + // Do a 'nearbySearch' followed by 'getDetails' to avoid using + // GMap Geocoder which the user may not have enabled... but + // ideally Geocoder should be used to get the exact location at + // those coordinates and to limit billing query count. + location: location, + radius: 1, + }, (results, status) => { + const GMAP_CRITICAL_ERRORS = [google.maps.places.PlacesServiceStatus.REQUEST_DENIED, google.maps.places.PlacesServiceStatus.UNKNOWN_ERROR]; + if (status === google.maps.places.PlacesServiceStatus.OK) { + service.getDetails({ + placeId: results[0].place_id, + fields: ['geometry', 'formatted_address'], + }, (place, status) => { + resolve(); + if (status === google.maps.places.PlacesServiceStatus.OK) { + this._gmapCacheGPSToPlace[gps] = place; + this._gmapPlace = place; + } else if (GMAP_CRITICAL_ERRORS.includes(status)) { + this.trigger_up('user_value_widget_critical'); + } + }); + } else if (GMAP_CRITICAL_ERRORS.includes(status)) { + resolve(); + this.trigger_up('user_value_widget_critical'); + } + }); + }); + if (this._gmapPlace) { + this.inputEl.value = this._gmapPlace.formatted_address; + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onPlaceChanged(ev) { + const gmapPlace = this._gmapAutocomplete.getPlace(); + if (gmapPlace && gmapPlace.geometry) { + this._gmapPlace = gmapPlace; + const location = this._gmapPlace.geometry.location; + this._value = `(${location.lat()},${location.lng()})`; + this._gmapCacheGPSToPlace[this._value] = gmapPlace; + this._onUserValueChange(ev); + } + }, +}); + +options.userValueWidgetsRegistry['we-urlpicker'] = UrlPickerUserValueWidget; +options.userValueWidgetsRegistry['we-fontfamilypicker'] = FontFamilyPickerUserValueWidget; +options.userValueWidgetsRegistry['we-gpspicker'] = GPSPicker; + +//:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: + +options.Class.include({ + xmlDependencies: (options.Class.prototype.xmlDependencies || []) + .concat(['/website/static/src/xml/website.editor.xml']), + custom_events: _.extend({}, options.Class.prototype.custom_events || {}, { + 'google_fonts_custo_request': '_onGoogleFontsCustoRequest', + }), + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for parameters + */ + customizeWebsiteViews: async function (previewMode, widgetValue, params) { + await this._customizeWebsite(previewMode, widgetValue, params, 'views'); + }, + /** + * @see this.selectClass for parameters + */ + customizeWebsiteVariable: async function (previewMode, widgetValue, params) { + await this._customizeWebsite(previewMode, widgetValue, params, 'variable'); + }, + /** + * @see this.selectClass for parameters + */ + customizeWebsiteColor: async function (previewMode, widgetValue, params) { + await this._customizeWebsite(previewMode, widgetValue, params, 'color'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _checkIfWidgetsUpdateNeedReload(widgets) { + const needReload = await this._super(...arguments); + if (needReload) { + return needReload; + } + for (const widget of widgets) { + const methodsNames = widget.getMethodsNames(); + if (!methodsNames.includes('customizeWebsiteViews') + && !methodsNames.includes('customizeWebsiteVariable') + && !methodsNames.includes('customizeWebsiteColor')) { + continue; + } + let paramsReload = false; + if (widget.getMethodsParams('customizeWebsiteViews').reload + || widget.getMethodsParams('customizeWebsiteVariable').reload + || widget.getMethodsParams('customizeWebsiteColor').reload) { + paramsReload = true; + } + if (paramsReload || config.isDebug('assets')) { + return (config.isDebug('assets') ? _t("It appears you are in debug=assets mode, all theme customization options require a page reload in this mode.") : true); + } + } + return false; + }, + /** + * @override + */ + _computeWidgetState: async function (methodName, params) { + switch (methodName) { + case 'customizeWebsiteViews': { + const allXmlIDs = this._getXMLIDsFromPossibleValues(params.possibleValues); + const enabledXmlIDs = await this._rpc({ + route: '/website/theme_customize_get', + params: { + 'xml_ids': allXmlIDs, + }, + }); + let mostXmlIDsStr = ''; + let mostXmlIDsNb = 0; + for (const xmlIDsStr of params.possibleValues) { + const enableXmlIDs = xmlIDsStr.split(/\s*,\s*/); + if (enableXmlIDs.length > mostXmlIDsNb + && enableXmlIDs.every(xmlID => enabledXmlIDs.includes(xmlID))) { + mostXmlIDsStr = xmlIDsStr; + mostXmlIDsNb = enableXmlIDs.length; + } + } + return mostXmlIDsStr; // Need to return the exact same string as in possibleValues + } + case 'customizeWebsiteVariable': { + return weUtils.getCSSVariableValue(params.variable); + } + case 'customizeWebsiteColor': { + // TODO adapt in master + const bugfixedValue = weUtils.getCSSVariableValue(`bugfixed-${params.color}`); + if (bugfixedValue) { + return bugfixedValue; + } + return weUtils.getCSSVariableValue(params.color); + } + } + return this._super(...arguments); + }, + /** + * @private + */ + _customizeWebsite: async function (previewMode, widgetValue, params, type) { + // Never allow previews for theme customizations + if (previewMode) { + return; + } + + switch (type) { + case 'views': + await this._customizeWebsiteViews(widgetValue, params); + break; + case 'variable': + await this._customizeWebsiteVariable(widgetValue, params); + break; + case 'color': + await this._customizeWebsiteColor(widgetValue, params); + break; + } + + if (params.reload || config.isDebug('assets')) { + // Caller will reload the page, nothing needs to be done anymore. + return; + } + + // Finally, only update the bundles as no reload is required + await this._reloadBundles(); + + // Some public widgets may depend on the variables that were + // customized, so we have to restart them *all*. + await new Promise((resolve, reject) => { + this.trigger_up('widgets_start_request', { + editableMode: true, + onSuccess: () => resolve(), + onFailure: () => reject(), + }); + }); + }, + /** + * @private + */ + _customizeWebsiteColor: async function (color, params) { + const baseURL = '/website/static/src/scss/options/colors/'; + const colorType = params.colorType ? (params.colorType + '_') : ''; + const url = `${baseURL}user_${colorType}color_palette.scss`; + + if (color) { + if (weUtils.isColorCombinationName(color)) { + color = parseInt(color); + } else if (!ColorpickerWidget.isCSSColor(color)) { + color = `'${color}'`; + } + } + return this._makeSCSSCusto(url, {[params.color]: color}); + }, + /** + * @private + */ + _customizeWebsiteVariable: async function (value, params) { + return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', { + [params.variable]: value, + }); + }, + /** + * @private + */ + _customizeWebsiteViews: async function (xmlID, params) { + const allXmlIDs = this._getXMLIDsFromPossibleValues(params.possibleValues); + const enableXmlIDs = xmlID.split(/\s*,\s*/); + const disableXmlIDs = allXmlIDs.filter(xmlID => !enableXmlIDs.includes(xmlID)); + + return this._rpc({ + route: '/website/theme_customize', + params: { + 'enable': enableXmlIDs, + 'disable': disableXmlIDs, + }, + }); + }, + /** + * @private + */ + _getXMLIDsFromPossibleValues: function (possibleValues) { + const allXmlIDs = []; + for (const xmlIDsStr of possibleValues) { + allXmlIDs.push(...xmlIDsStr.split(/\s*,\s*/)); + } + return allXmlIDs.filter((v, i, arr) => arr.indexOf(v) === i); + }, + /** + * @private + */ + _makeSCSSCusto: async function (url, values) { + return this._rpc({ + route: '/website/make_scss_custo', + params: { + 'url': url, + 'values': _.mapObject(values, v => v || 'null'), + }, + }); + }, + /** + * Refreshes all public widgets related to the given element. + * + * @private + * @param {jQuery} [$el=this.$target] + * @returns {Promise} + */ + _refreshPublicWidgets: async function ($el) { + return new Promise((resolve, reject) => { + this.trigger_up('widgets_start_request', { + editableMode: true, + $target: $el || this.$target, + onSuccess: resolve, + onFailure: reject, + }); + }); + }, + /** + * @private + */ + _reloadBundles: async function () { + const bundles = await this._rpc({ + route: '/website/theme_customize_bundle_reload', + }); + let $allLinks = $(); + const proms = _.map(bundles, (bundleURLs, bundleName) => { + var $links = $('link[href*="' + bundleName + '"]'); + $allLinks = $allLinks.add($links); + var $newLinks = $(); + _.each(bundleURLs, url => { + $newLinks = $newLinks.add($('<link/>', { + type: 'text/css', + rel: 'stylesheet', + href: url, + })); + }); + + const linksLoaded = new Promise(resolve => { + let nbLoaded = 0; + $newLinks.on('load error', () => { // If we have an error, just ignore it + if (++nbLoaded >= $newLinks.length) { + resolve(); + } + }); + }); + $links.last().after($newLinks); + return linksLoaded; + }); + await Promise.all(proms).then(() => $allLinks.remove()); + }, + /** + * @override + */ + _select: async function (previewMode, widget) { + await this._super(...arguments); + + if (!widget.$el.closest('[data-no-widget-refresh="true"]').length) { + // TODO the flag should be retrieved through widget params somehow + await this._refreshPublicWidgets(); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} ev + */ + _onGoogleFontsCustoRequest: function (ev) { + const values = ev.data.values ? _.clone(ev.data.values) : {}; + const googleFonts = ev.data.googleFonts; + if (googleFonts.length) { + values['google-fonts'] = "('" + googleFonts.join("', '") + "')"; + } else { + values['google-fonts'] = 'null'; + } + this.trigger_up('snippet_edition_request', {exec: async () => { + return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values); + }}); + this.trigger_up('request_save', { + reloadEditor: true, + }); + }, +}); + +function _getLastPreFilterLayerElement($el) { + // Make sure parallax and video element are considered to be below the + // color filters / shape + const $bgVideo = $el.find('> .o_bg_video_container'); + if ($bgVideo.length) { + return $bgVideo[0]; + } + const $parallaxEl = $el.find('> .s_parallax_bg'); + if ($parallaxEl.length) { + return $parallaxEl[0]; + } + return null; +} + +options.registry.BackgroundToggler.include({ + /** + * Toggles background video on or off. + * + * @see this.selectClass for parameters + */ + toggleBgVideo(previewMode, widgetValue, params) { + if (!widgetValue) { + // TODO: use setWidgetValue instead of calling background directly when possible + const [bgVideoWidget] = this._requestUserValueWidgets('bg_video_opt'); + const bgVideoOpt = bgVideoWidget.getParent(); + return bgVideoOpt._setBgVideo(false, ''); + } else { + // TODO: use trigger instead of el.click when possible + this._requestUserValueWidgets('bg_video_opt')[0].el.click(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState(methodName, params) { + if (methodName === 'toggleBgVideo') { + return this.$target[0].classList.contains('o_background_video'); + } + return this._super(...arguments); + }, + /** + * TODO an overall better management of background layers is needed + * + * @override + */ + _getLastPreFilterLayerElement() { + const el = _getLastPreFilterLayerElement(this.$target); + if (el) { + return el; + } + return this._super(...arguments); + }, +}); + +options.registry.BackgroundShape.include({ + /** + * TODO need a better management of background layers + * + * @override + */ + _getLastPreShapeLayerElement() { + const el = this._super(...arguments); + if (el) { + return el; + } + return _getLastPreFilterLayerElement(this.$target); + } +}); + +options.registry.BackgroundVideo = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Sets the target's background video. + * + * @see this.selectClass for parameters + */ + background: function (previewMode, widgetValue, params) { + if (previewMode === 'reset' && this.videoSrc) { + return this._setBgVideo(false, this.videoSrc); + } + return this._setBgVideo(previewMode, widgetValue); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + if (methodName === 'background') { + if (this.$target[0].classList.contains('o_background_video')) { + return this.$('> .o_bg_video_container iframe').attr('src'); + } + return ''; + } + return this._super(...arguments); + }, + /** + * Updates the background video used by the snippet. + * + * @private + * @see this.selectClass for parameters + * @returns {Promise} + */ + _setBgVideo: async function (previewMode, value) { + this.$('> .o_bg_video_container').toggleClass('d-none', previewMode === true); + + if (previewMode !== false) { + return; + } + + this.videoSrc = value; + var target = this.$target[0]; + target.classList.toggle('o_background_video', !!(value && value.length)); + if (value && value.length) { + target.dataset.bgVideoSrc = value; + } else { + delete target.dataset.bgVideoSrc; + } + await this._refreshPublicWidgets(); + }, +}); + +options.registry.OptionsTab = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for parameters + */ + async configureApiKey(previewMode, widgetValue, params) { + return new Promise(resolve => { + this.trigger_up('gmap_api_key_request', { + editableMode: true, + reconfigure: true, + onSuccess: () => resolve(), + }); + }); + }, + /** + * @see this.selectClass for parameters + */ + async customizeBodyBgType(previewMode, widgetValue, params) { + if (widgetValue === 'NONE') { + this.bodyImageType = 'image'; + return this.customizeBodyBg(previewMode, '', params); + } + // TODO improve: hack to click on external image picker + this.bodyImageType = widgetValue; + const widget = this._requestUserValueWidgets(params.imagepicker)[0]; + widget.enable(); + }, + /** + * @override + */ + async customizeBodyBg(previewMode, widgetValue, params) { + // TODO improve: customize two variables at the same time... + await this.customizeWebsiteVariable(previewMode, this.bodyImageType, {variable: 'body-image-type'}); + await this.customizeWebsiteVariable(previewMode, widgetValue ? `'${widgetValue}'` : '', {variable: 'body-image'}); + }, + /** + * @see this.selectClass for parameters + */ + async openCustomCodeDialog(previewMode, widgetValue, params) { + const libsProm = this._loadLibs({ + jsLibs: [ + '/web/static/lib/ace/ace.js', + '/web/static/lib/ace/mode-xml.js', + ], + }); + + let websiteId; + this.trigger_up('context_get', { + callback: (ctx) => { + websiteId = ctx['website_id']; + }, + }); + + let website; + const dataProm = this._rpc({ + model: 'website', + method: 'read', + args: [[websiteId], ['custom_code_head', 'custom_code_footer']], + }).then(websites => { + website = websites[0]; + }); + + let fieldName, title, contentText; + if (widgetValue === 'head') { + fieldName = 'custom_code_head'; + title = _t('Custom head code'); + contentText = _t('Enter code that will be added into the <head> of every page of your site.'); + } else { + fieldName = 'custom_code_footer'; + title = _t('Custom end of body code'); + contentText = _t('Enter code that will be added before the </body> of every page of your site.'); + } + + await Promise.all([libsProm, dataProm]); + + await new Promise(resolve => { + const $content = $(core.qweb.render('website.custom_code_dialog_content', { + contentText, + })); + const aceEditor = this._renderAceEditor($content.find('.o_ace_editor_container')[0], website[fieldName] || ''); + const dialog = new Dialog(this, { + title, + $content, + buttons: [ + { + text: _t("Save"), + classes: 'btn-primary', + click: async () => { + await this._rpc({ + model: 'website', + method: 'write', + args: [ + [websiteId], + {[fieldName]: aceEditor.getValue()}, + ], + }); + }, + close: true, + }, + { + text: _t("Discard"), + close: true, + }, + ], + }); + dialog.on('closed', this, resolve); + dialog.open(); + }); + }, + /** + * @see this.selectClass for parameters + */ + async switchTheme(previewMode, widgetValue, params) { + const save = await new Promise(resolve => { + Dialog.confirm(this, _t("Changing theme requires to leave the editor. This will save all your changes, are you sure you want to proceed? Be careful that changing the theme will reset all your color customizations."), { + confirm_callback: () => resolve(true), + cancel_callback: () => resolve(false), + }); + }); + if (!save) { + return; + } + this.trigger_up('request_save', { + reload: false, + onSuccess: () => window.location.href = '/web#action=website.theme_install_kanban_action', + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _checkIfWidgetsUpdateNeedWarning(widgets) { + const warningMessage = await this._super(...arguments); + if (warningMessage) { + return warningMessage; + } + for (const widget of widgets) { + if (widget.getMethodsNames().includes('customizeWebsiteVariable') + && widget.getMethodsParams('customizeWebsiteVariable').variable === 'color-palettes-number') { + const hasCustomizedColors = weUtils.getCSSVariableValue('has-customized-colors'); + if (hasCustomizedColors && hasCustomizedColors !== 'false') { + return _t("Changing the color palette will reset all your color customizations, are you sure you want to proceed?"); + } + } + } + return ''; + }, + /** + * @override + */ + async _computeWidgetState(methodName, params) { + if (methodName === 'customizeBodyBgType') { + const bgImage = $('#wrapwrap').css('background-image'); + if (bgImage === 'none') { + return "NONE"; + } + return weUtils.getCSSVariableValue('body-image-type'); + } + return this._super(...arguments); + }, + /** + * @override + */ + async _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'body_bg_image_opt') { + return false; + } + return this._super(...arguments); + }, + /** + * @private + * @param {DOMElement} node + * @param {String} content text of the editor + * @returns {Object} + */ + _renderAceEditor(node, content) { + const aceEditor = window.ace.edit(node); + aceEditor.setTheme('ace/theme/monokai'); + aceEditor.setValue(content, 1); + aceEditor.setOptions({ + minLines: 20, + maxLines: Infinity, + showPrintMargin: false, + }); + aceEditor.renderer.setOptions({ + highlightGutterLine: true, + showInvisibles: true, + fontSize: 14, + }); + + const aceSession = aceEditor.getSession(); + aceSession.setOptions({ + mode: "ace/mode/xml", + useWorker: false, + }); + return aceEditor; + }, + /** + * @override + */ + async _renderCustomXML(uiFragment) { + uiFragment.querySelectorAll('we-colorpicker').forEach(el => { + el.dataset.lazyPalette = 'true'; + }); + }, +}); + +options.registry.ThemeColors = options.registry.OptionsTab.extend({ + /** + * @override + */ + async start() { + // Checks for support of the old color system + const style = window.getComputedStyle(document.documentElement); + const supportOldColorSystem = weUtils.getCSSVariableValue('support-13-0-color-system', style) === 'true'; + const hasCustomizedOldColorSystem = weUtils.getCSSVariableValue('has-customized-13-0-color-system', style) === 'true'; + this._showOldColorSystemWarning = supportOldColorSystem && hasCustomizedOldColorSystem; + + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + async updateUIVisibility() { + await this._super(...arguments); + const oldColorSystemEl = this.el.querySelector('.o_old_color_system_warning'); + oldColorSystemEl.classList.toggle('d-none', !this._showOldColorSystemWarning); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _renderCustomXML(uiFragment) { + const paletteSelectorEl = uiFragment.querySelector('[data-variable="color-palettes-number"]'); + const style = window.getComputedStyle(document.documentElement); + const nbPalettes = parseInt(weUtils.getCSSVariableValue('number-of-color-palettes', style)); + for (let i = 1; i <= nbPalettes; i++) { + const btnEl = document.createElement('we-button'); + btnEl.classList.add('o_palette_color_preview_button'); + btnEl.dataset.customizeWebsiteVariable = i; + for (let c = 1; c <= 5; c++) { + const colorPreviewEl = document.createElement('span'); + colorPreviewEl.classList.add('o_palette_color_preview'); + const color = weUtils.getCSSVariableValue(`o-palette-${i}-o-color-${c}`, style); + colorPreviewEl.style.backgroundColor = color; + btnEl.appendChild(colorPreviewEl); + } + paletteSelectorEl.appendChild(btnEl); + } + + for (let i = 1; i <= 5; i++) { + const collapseEl = document.createElement('we-collapse'); + const ccPreviewEl = $(qweb.render('web_editor.color.combination.preview'))[0]; + ccPreviewEl.classList.add('text-center', `o_cc${i}`); + collapseEl.appendChild(ccPreviewEl); + const editionEls = $(qweb.render('website.color_combination_edition', {number: i})); + for (const el of editionEls) { + collapseEl.appendChild(el); + } + uiFragment.appendChild(collapseEl); + } + + await this._super(...arguments); + }, +}); + +options.registry.menu_data = options.Class.extend({ + /** + * When the users selects a menu, a dialog is opened to ask him if he wants + * to follow the link (and leave editor), edit the menu or do nothing. + * + * @override + */ + onFocus: function () { + var self = this; + (new Dialog(this, { + title: _t("Confirmation"), + $content: $(core.qweb.render('website.leaving_current_page_edition')), + buttons: [ + {text: _t("Go to Link"), classes: 'btn-primary', click: function () { + self.trigger_up('request_save', { + reload: false, + onSuccess: function () { + window.location.href = self.$target.attr('href'); + }, + }); + }}, + {text: _t("Edit the menu"), classes: 'btn-primary', click: function () { + this.trigger_up('action_demand', { + actionName: 'edit_menu', + params: [ + function () { + var prom = new Promise(function (resolve, reject) { + self.trigger_up('request_save', { + onSuccess: resolve, + onFailure: reject, + }); + }); + return prom; + }, + ], + }); + }}, + {text: _t("Stay on this page"), close: true} + ] + })).open(); + }, +}); + +options.registry.company_data = options.Class.extend({ + /** + * Fetches data to determine the URL where the user can edit its company + * data. Saves the info in the prototype to do this only once. + * + * @override + */ + start: function () { + var proto = options.registry.company_data.prototype; + var prom; + var self = this; + if (proto.__link === undefined) { + prom = this._rpc({route: '/web/session/get_session_info'}).then(function (session) { + return self._rpc({ + model: 'res.users', + method: 'read', + args: [session.uid, ['company_id']], + }); + }).then(function (res) { + proto.__link = '/web#action=base.action_res_company_form&view_type=form&id=' + (res && res[0] && res[0].company_id[0] || 1); + }); + } + return Promise.all([this._super.apply(this, arguments), prom]); + }, + /** + * When the users selects company data, opens a dialog to ask him if he + * wants to be redirected to the company form view to edit it. + * + * @override + */ + onFocus: function () { + var self = this; + var proto = options.registry.company_data.prototype; + + Dialog.confirm(this, _t("Do you want to edit the company data ?"), { + confirm_callback: function () { + self.trigger_up('request_save', { + reload: false, + onSuccess: function () { + window.location.href = proto.__link; + }, + }); + }, + }); + }, +}); + +options.registry.Carousel = options.Class.extend({ + /** + * @override + */ + start: function () { + this.$target.carousel('pause'); + this.$indicators = this.$target.find('.carousel-indicators'); + this.$controls = this.$target.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators'); + + // Prevent enabling the carousel overlay when clicking on the carousel + // controls (indeed we want it to change the carousel slide then enable + // the slide overlay) + See "CarouselItem" option. + this.$controls.addClass('o_we_no_overlay'); + + let _slideTimestamp; + this.$target.on('slide.bs.carousel.carousel_option', () => { + _slideTimestamp = window.performance.now(); + setTimeout(() => this.trigger_up('hide_overlay')); + }); + this.$target.on('slid.bs.carousel.carousel_option', () => { + // slid.bs.carousel is most of the time fired too soon by bootstrap + // since it emulates the transitionEnd with a setTimeout. We wait + // here an extra 20% of the time before retargeting edition, which + // should be enough... + const _slideDuration = (window.performance.now() - _slideTimestamp); + setTimeout(() => { + this.trigger_up('activate_snippet', { + $snippet: this.$target.find('.carousel-item.active'), + ifInactiveOptions: true, + }); + this.$target.trigger('active_slide_targeted'); + }, 0.2 * _slideDuration); + }); + + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + this.$target.off('.carousel_option'); + }, + /** + * @override + */ + onBuilt: function () { + this._assignUniqueID(); + }, + /** + * @override + */ + onClone: function () { + this._assignUniqueID(); + }, + /** + * @override + */ + cleanForSave: function () { + const $items = this.$target.find('.carousel-item'); + $items.removeClass('next prev left right active').first().addClass('active'); + this.$indicators.find('li').removeClass('active').empty().first().addClass('active'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Creates a unique ID for the carousel and reassign data-attributes that + * depend on it. + * + * @private + */ + _assignUniqueID: function () { + const id = 'myCarousel' + Date.now(); + this.$target.attr('id', id); + this.$target.find('[data-target]').attr('data-target', '#' + id); + _.each(this.$target.find('[data-slide], [data-slide-to]'), function (el) { + var $el = $(el); + if ($el.attr('data-target')) { + $el.attr('data-target', '#' + id); + } else if ($el.attr('href')) { + $el.attr('href', '#' + id); + } + }); + }, +}); + +options.registry.CarouselItem = options.Class.extend({ + isTopOption: true, + forceNoDeleteButton: true, + + /** + * @override + */ + start: function () { + this.$carousel = this.$target.closest('.carousel'); + this.$indicators = this.$carousel.find('.carousel-indicators'); + this.$controls = this.$carousel.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators'); + + var leftPanelEl = this.$overlay.data('$optionsSection')[0]; + var titleTextEl = leftPanelEl.querySelector('we-title > span'); + this.counterEl = document.createElement('span'); + titleTextEl.appendChild(this.counterEl); + + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this._super(...arguments); + this.$carousel.off('.carousel_item_option'); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Updates the slide counter. + * + * @override + */ + updateUI: async function () { + await this._super(...arguments); + const $items = this.$carousel.find('.carousel-item'); + const $activeSlide = $items.filter('.active'); + const updatedText = ` (${$activeSlide.index() + 1}/${$items.length})`; + this.counterEl.textContent = updatedText; + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Adds a slide. + * + * @see this.selectClass for parameters + */ + addSlide: function (previewMode) { + const $items = this.$carousel.find('.carousel-item'); + this.$controls.removeClass('d-none'); + this.$indicators.append($('<li>', { + 'data-target': '#' + this.$carousel.attr('id'), + 'data-slide-to': $items.length, + })); + this.$indicators.append(' '); + // Need to remove editor data from the clone so it gets its own. + const $active = $items.filter('.active'); + $active.clone(false) + .removeClass('active') + .insertAfter($active); + this.$carousel.carousel('next'); + }, + /** + * Removes the current slide. + * + * @see this.selectClass for parameters. + */ + removeSlide: function (previewMode) { + const $items = this.$carousel.find('.carousel-item'); + const newLength = $items.length - 1; + if (!this.removing && newLength > 0) { + const $toDelete = $items.filter('.active'); + this.$carousel.one('active_slide_targeted.carousel_item_option', () => { + $toDelete.remove(); + this.$indicators.find('li:last').remove(); + this.$controls.toggleClass('d-none', newLength === 1); + this.$carousel.trigger('content_changed'); + this.removing = false; + }); + this.removing = true; + this.$carousel.carousel('prev'); + } + }, + /** + * Goes to next slide or previous slide. + * + * @see this.selectClass for parameters + */ + slide: function (previewMode, widgetValue, params) { + switch (widgetValue) { + case 'left': + this.$controls.filter('.carousel-control-prev')[0].click(); + break; + case 'right': + this.$controls.filter('.carousel-control-next')[0].click(); + break; + } + }, +}); + +options.registry.sizing_x = options.registry.sizing.extend({ + /** + * @override + */ + onClone: function (options) { + this._super.apply(this, arguments); + // Below condition is added to remove offset of target element only + // and not its children to avoid design alteration of a container/block. + if (options.isCurrent) { + var _class = this.$target.attr('class').replace(/\s*(offset-xl-|offset-lg-)([0-9-]+)/g, ''); + this.$target.attr('class', _class); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _getSize: function () { + var width = this.$target.closest('.row').width(); + var gridE = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + var gridW = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + this.grid = { + e: [_.map(gridE, v => ('col-lg-' + v)), _.map(gridE, v => width / 12 * v), 'width'], + w: [_.map(gridW, v => ('offset-lg-' + v)), _.map(gridW, v => width / 12 * v), 'margin-left'], + }; + return this.grid; + }, + /** + * @override + */ + _onResize: function (compass, beginClass, current) { + if (compass === 'w') { + // don't change the right border position when we change the offset (replace col size) + var beginCol = Number(beginClass.match(/col-lg-([0-9]+)|$/)[1] || 0); + var beginOffset = Number(beginClass.match(/offset-lg-([0-9-]+)|$/)[1] || beginClass.match(/offset-xl-([0-9-]+)|$/)[1] || 0); + var offset = Number(this.grid.w[0][current].match(/offset-lg-([0-9-]+)|$/)[1] || 0); + if (offset < 0) { + offset = 0; + } + var colSize = beginCol - (offset - beginOffset); + if (colSize <= 0) { + colSize = 1; + offset = beginOffset + beginCol - 1; + } + this.$target.attr('class', this.$target.attr('class').replace(/\s*(offset-xl-|offset-lg-|col-lg-)([0-9-]+)/g, '')); + + this.$target.addClass('col-lg-' + (colSize > 12 ? 12 : colSize)); + if (offset > 0) { + this.$target.addClass('offset-lg-' + offset); + } + } + this._super.apply(this, arguments); + }, +}); + +options.registry.layout_column = options.Class.extend({ + /** + * @override + */ + start: function () { + // Needs to be done manually for now because _computeWidgetVisibility + // doesn't go through this option for buttons inside of a select. + // TODO: improve this. + this.$el.find('we-button[data-name="zero_cols_opt"]') + .toggleClass('d-none', !this.$target.is('.s_allow_columns')); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Changes the number of columns. + * + * @see this.selectClass for parameters + */ + selectCount: async function (previewMode, widgetValue, params) { + const previousNbColumns = this.$('> .row').children().length; + let $row = this.$('> .row'); + if (!$row.length) { + $row = this.$target.contents().wrapAll($('<div class="row"><div class="col-lg-12"/></div>')).parent().parent(); + } + + const nbColumns = parseInt(widgetValue); + await this._updateColumnCount($row, (nbColumns || 1) - $row.children().length); + // Yield UI thread to wait for event to bubble before activate_snippet is called. + // In this case this lets the select handle the click event before we switch snippet. + // TODO: make this more generic in activate_snippet event handler. + await new Promise(resolve => setTimeout(resolve)); + if (nbColumns === 0) { + $row.contents().unwrap().contents().unwrap(); + this.trigger_up('activate_snippet', {$snippet: this.$target}); + } else if (previousNbColumns === 0) { + this.trigger_up('activate_snippet', {$snippet: this.$('> .row').children().first()}); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + if (methodName === 'selectCount') { + return this.$('> .row').children().length; + } + return this._super(...arguments); + }, + /** + * Adds new columns which are clones of the last column or removes the + * last x columns. + * + * @private + * @param {jQuery} $row - the row in which to update the columns + * @param {integer} count - positif to add, negative to remove + */ + _updateColumnCount: async function ($row, count) { + if (!count) { + return; + } + + if (count > 0) { + var $lastColumn = $row.children().last(); + for (var i = 0; i < count; i++) { + await new Promise(resolve => { + this.trigger_up('clone_snippet', {$snippet: $lastColumn, onSuccess: resolve}); + }); + } + } else { + var self = this; + for (const el of $row.children().slice(count)) { + await new Promise(resolve => { + self.trigger_up('remove_snippet', {$snippet: $(el), onSuccess: resolve}); + }); + } + } + + this._resizeColumns($row.children()); + this.trigger_up('cover_update'); + }, + /** + * Resizes the columns so that they are kept on one row. + * + * @private + * @param {jQuery} $columns - the columns to resize + */ + _resizeColumns: function ($columns) { + const colsLength = $columns.length; + var colSize = Math.floor(12 / colsLength) || 1; + var colOffset = Math.floor((12 - colSize * colsLength) / 2); + var colClass = 'col-lg-' + colSize; + _.each($columns, function (column) { + var $column = $(column); + $column.attr('class', $column.attr('class').replace(/\b(col|offset)-lg(-\d+)?\b/g, '')); + $column.addClass(colClass); + }); + if (colOffset) { + $columns.first().addClass('offset-lg-' + colOffset); + } + }, +}); + +options.registry.Parallax = options.Class.extend({ + /** + * @override + */ + async start() { + this.parallaxEl = this.$target.find('> .s_parallax_bg')[0] || null; + this._updateBackgroundOptions(); + + this.$target.on('content_changed.ParallaxOption', this._onExternalUpdate.bind(this)); + + return this._super(...arguments); + }, + /** + * @override + */ + onFocus() { + // Refresh the parallax animation on focus; at least useful because + // there may have been changes in the page that influenced the parallax + // rendering (new snippets, ...). + // TODO make this automatic. + if (this.parallaxEl) { + this._refreshPublicWidgets(); + } + }, + /** + * @override + */ + onMove() { + this._refreshPublicWidgets(); + }, + /** + * @override + */ + destroy() { + this._super(...arguments); + this.$target.off('.ParallaxOption'); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Build/remove parallax. + * + * @see this.selectClass for parameters + */ + async selectDataAttribute(previewMode, widgetValue, params) { + await this._super(...arguments); + if (params.attributeName !== 'scrollBackgroundRatio') { + return; + } + + const isParallax = (widgetValue !== '0'); + this.$target.toggleClass('parallax', isParallax); + this.$target.toggleClass('s_parallax_is_fixed', widgetValue === '1'); + this.$target.toggleClass('s_parallax_no_overflow_hidden', (widgetValue === '0' || widgetValue === '1')); + if (isParallax) { + if (!this.parallaxEl) { + this.parallaxEl = document.createElement('span'); + this.parallaxEl.classList.add('s_parallax_bg'); + this.$target.prepend(this.parallaxEl); + } + } else { + if (this.parallaxEl) { + this.parallaxEl.remove(); + this.parallaxEl = null; + } + } + + this._updateBackgroundOptions(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _computeVisibility(widgetName) { + return !this.$target.hasClass('o_background_video'); + }, + /** + * @override + */ + async _computeWidgetState(methodName, params) { + if (methodName === 'selectDataAttribute' && params.parallaxTypeOpt) { + const attrName = params.attributeName; + const attrValue = (this.$target[0].dataset[attrName] || params.attributeDefaultValue).trim(); + switch (attrValue) { + case '0': + case '1': { + return attrValue; + } + default: { + return (attrValue.startsWith('-') ? '-1.5' : '1.5'); + } + } + } + return this._super(...arguments); + }, + /** + * Updates external background-related option to work with the parallax + * element instead of the original target when necessary. + * + * @private + */ + _updateBackgroundOptions() { + this.trigger_up('option_update', { + optionNames: ['BackgroundImage', 'BackgroundPosition', 'BackgroundOptimize'], + name: 'target', + data: this.parallaxEl ? $(this.parallaxEl) : this.$target, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called on any snippet update to check if the parallax should still be + * enabled or not. + * + * TODO there is probably a better system to implement to solve this issue. + * + * @private + * @param {Event} ev + */ + _onExternalUpdate(ev) { + if (!this.parallaxEl) { + return; + } + const bgImage = this.parallaxEl.style.backgroundImage; + if (!bgImage || bgImage === 'none' || this.$target.hasClass('o_background_video')) { + // The parallax option was enabled but the background image was + // removed: disable the parallax option. + const widget = this._requestUserValueWidgets('parallax_none_opt')[0]; + widget.enable(); + widget.getParent().close(); // FIXME remove this ugly hack asap + } + }, +}); + +options.registry.collapse = options.Class.extend({ + /** + * @override + */ + start: function () { + var self = this; + this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () { + self.trigger_up('cover_update'); + self.$target.trigger('content_changed'); + }); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + onBuilt: function () { + this._createIDs(); + }, + /** + * @override + */ + onClone: function () { + this._createIDs(); + }, + /** + * @override + */ + onMove: function () { + this._createIDs(); + var $panel = this.$target.find('.collapse').removeData('bs.collapse'); + if ($panel.attr('aria-expanded') === 'true') { + $panel.closest('.accordion').find('.collapse[aria-expanded="true"]') + .filter((i, el) => (el !== $panel[0])) + .collapse('hide') + .one('hidden.bs.collapse', function () { + $panel.trigger('shown.bs.collapse'); + }); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Associates unique ids on collapse elements. + * + * @private + */ + _createIDs: function () { + let time = new Date().getTime(); + const $tablist = this.$target.closest('[role="tablist"]'); + const $tab = this.$target.find('[role="tab"]'); + const $panel = this.$target.find('[role="tabpanel"]'); + + const setUniqueId = ($elem, label) => { + let elemId = $elem.attr('id'); + if (!elemId || $('[id="' + elemId + '"]').length > 1) { + do { + time++; + elemId = label + time; + } while ($('#' + elemId).length); + $elem.attr('id', elemId); + } + return elemId; + }; + + const tablistId = setUniqueId($tablist, 'myCollapse'); + $panel.attr('data-parent', '#' + tablistId); + $panel.data('parent', '#' + tablistId); + + const panelId = setUniqueId($panel, 'myCollapseTab'); + $tab.attr('data-target', '#' + panelId); + $tab.data('target', '#' + panelId); + }, +}); + +options.registry.HeaderNavbar = options.Class.extend({ + /** + * Particular case: we want the option to be associated on the header navbar + * in XML so that the related options only appear on navbar click (not + * header), in a different section, etc... but we still want the target to + * be the header itself. + * + * @constructor + */ + init() { + this._super(...arguments); + // Don't use setTarget, we want it to be set directly at initialization. + this.$target = this.$target.closest('#wrapwrap > header'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Needs to be done manually for now because data-dependencies + * doesn't work with "AND" conditions. + * TODO: improve this. + * + * @override + */ + async _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'option_logo_height_scrolled') { + return !this.$('.navbar-brand').hasClass('d-none'); + } + return this._super(...arguments); + }, +}); + +const VisibilityPageOptionUpdate = options.Class.extend({ + pageOptionName: undefined, + showOptionWidgetName: undefined, + shownValue: '', + + /** + * @override + */ + async start() { + await this._super(...arguments); + const shown = await this._isShown(); + this.trigger_up('snippet_option_visibility_update', {show: shown}); + }, + /** + * @override + */ + async onTargetShow() { + if (await this._isShown()) { + // onTargetShow may be called even if the element is already shown. + // In most cases, this is not a problem but here it is as the code + // that follows clicks on the visibility checkbox regardless of its + // status. This avoids searching for that checkbox entirely. + return; + } + // TODO improve: here we make a hack so that if we make the invisible + // header appear for edition, its actual visibility for the page is + // toggled (otherwise it would be about editing an element which + // is actually never displayed on the page). + const widget = this._requestUserValueWidgets(this.showOptionWidgetName)[0]; + widget.enable(); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for params + */ + async visibility(previewMode, widgetValue, params) { + const show = (widgetValue !== 'hidden'); + await new Promise(resolve => { + this.trigger_up('action_demand', { + actionName: 'toggle_page_option', + params: [{name: this.pageOptionName, value: show}], + onSuccess: () => resolve(), + }); + }); + this.trigger_up('snippet_option_visibility_update', {show: show}); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _computeWidgetState(methodName, params) { + if (methodName === 'visibility') { + const shown = await this._isShown(); + return shown ? this.shownValue : 'hidden'; + } + return this._super(...arguments); + }, + /** + * @private + * @returns {boolean} + */ + async _isShown() { + return new Promise(resolve => { + this.trigger_up('action_demand', { + actionName: 'get_page_option', + params: [this.pageOptionName], + onSuccess: v => resolve(!!v), + }); + }); + }, +}); + +options.registry.TopMenuVisibility = VisibilityPageOptionUpdate.extend({ + pageOptionName: 'header_visible', + showOptionWidgetName: 'regular_header_visibility_opt', + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Handles the switching between 3 differents visibilities of the header. + * + * @see this.selectClass for params + */ + async visibility(previewMode, widgetValue, params) { + await this._super(...arguments); + await this._changeVisibility(widgetValue); + // TODO this is hacky but changing the header visibility may have an + // effect on features like FullScreenHeight which depend on viewport + // size so we simulate a resize. + $(window).trigger('resize'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _changeVisibility(widgetValue) { + const show = (widgetValue !== 'hidden'); + if (!show) { + return; + } + const transparent = (widgetValue === 'transparent'); + await new Promise(resolve => { + this.trigger_up('action_demand', { + actionName: 'toggle_page_option', + params: [{name: 'header_overlay', value: transparent}], + onSuccess: () => resolve(), + }); + }); + if (!transparent) { + return; + } + await new Promise(resolve => { + this.trigger_up('action_demand', { + actionName: 'toggle_page_option', + params: [{name: 'header_color', value: ''}], + onSuccess: () => resolve(), + }); + }); + }, + /** + * @override + */ + async _computeWidgetState(methodName, params) { + const _super = this._super.bind(this); + if (methodName === 'visibility') { + this.shownValue = await new Promise(resolve => { + this.trigger_up('action_demand', { + actionName: 'get_page_option', + params: ['header_overlay'], + onSuccess: v => resolve(v ? 'transparent' : 'regular'), + }); + }); + } + return _super(...arguments); + }, +}); + +options.registry.topMenuColor = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @override + */ + selectStyle(previewMode, widgetValue, params) { + this._super(...arguments); + const className = widgetValue ? (params.colorPrefix + widgetValue) : ''; + this.trigger_up('action_demand', { + actionName: 'toggle_page_option', + params: [{name: 'header_color', value: className}], + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeVisibility: async function () { + const show = await this._super(...arguments); + if (!show) { + return false; + } + return new Promise(resolve => { + this.trigger_up('action_demand', { + actionName: 'get_page_option', + params: ['header_overlay'], + onSuccess: value => resolve(!!value), + }); + }); + }, +}); + +/** + * Manage the visibility of snippets on mobile. + */ +options.registry.MobileVisibility = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Allows to show or hide the associated snippet in mobile display mode. + * + * @see this.selectClass for parameters + */ + showOnMobile(previewMode, widgetValue, params) { + const classes = `d-none d-md-${this.$target.css('display')}`; + this.$target.toggleClass(classes, !widgetValue); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + async _computeWidgetState(methodName, params) { + if (methodName === 'showOnMobile') { + const classList = [...this.$target[0].classList]; + return classList.includes('d-none') && + classList.some(className => className.startsWith('d-md-')) ? '' : 'true'; + } + return await this._super(...arguments); + }, +}); + +/** + * Hide/show footer in the current page. + */ +options.registry.HideFooter = VisibilityPageOptionUpdate.extend({ + pageOptionName: 'footer_visible', + showOptionWidgetName: 'hide_footer_page_opt', + shownValue: 'shown', +}); + +/** + * Handles the edition of snippet's anchor name. + */ +options.registry.anchor = options.Class.extend({ + isTopOption: true, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + start: function () { + // Generate anchor and copy it to clipboard on click, show the tooltip on success + this.$button = this.$el.find('we-button'); + const clipboard = new ClipboardJS(this.$button[0], {text: () => this._getAnchorLink()}); + clipboard.on('success', () => { + const anchor = decodeURIComponent(this._getAnchorLink()); + this.displayNotification({ + type: 'success', + message: _.str.sprintf(_t("Anchor copied to clipboard<br>Link: %s"), anchor), + buttons: [{text: _t("Edit"), click: () => this.openAnchorDialog(), primary: true}], + }); + }); + + return this._super.apply(this, arguments); + }, + /** + * @override + */ + onClone: function () { + this.$target.removeAttr('data-anchor'); + this.$target.filter(':not(.carousel)').removeAttr('id'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * @see this.selectClass for parameters + */ + openAnchorDialog: function (previewMode, widgetValue, params) { + var self = this; + var buttons = [{ + text: _t("Save & copy"), + classes: 'btn-primary', + click: function () { + var $input = this.$('.o_input_anchor_name'); + var anchorName = self._text2Anchor($input.val()); + if (self.$target[0].id === anchorName) { + // If the chosen anchor name is already the one used by the + // element, close the dialog and do nothing else + this.close(); + return; + } + + const alreadyExists = !!document.getElementById(anchorName); + this.$('.o_anchor_already_exists').toggleClass('d-none', !alreadyExists); + $input.toggleClass('is-invalid', alreadyExists); + if (!alreadyExists) { + self._setAnchorName(anchorName); + this.close(); + self.$button[0].click(); + } + }, + }, { + text: _t("Discard"), + close: true, + }]; + if (this.$target.attr('id')) { + buttons.push({ + text: _t("Remove"), + classes: 'btn-link ml-auto', + icon: 'fa-trash', + close: true, + click: function () { + self._setAnchorName(); + }, + }); + } + new Dialog(this, { + title: _t("Link Anchor"), + $content: $(qweb.render('website.dialog.anchorName', { + currentAnchor: decodeURIComponent(this.$target.attr('id')), + })), + buttons: buttons, + }).open(); + }, + /** + * @private + * @param {String} value + */ + _setAnchorName: function (value) { + if (value) { + this.$target.attr({ + 'id': value, + 'data-anchor': true, + }); + } else { + this.$target.removeAttr('id data-anchor'); + } + this.$target.trigger('content_changed'); + }, + /** + * Returns anchor text. + * + * @private + * @returns {string} + */ + _getAnchorLink: function () { + if (!this.$target[0].id) { + const $titles = this.$target.find('h1, h2, h3, h4, h5, h6'); + const title = $titles.length > 0 ? $titles[0].innerText : this.data.snippetName; + const anchorName = this._text2Anchor(title); + let n = ''; + while (document.getElementById(anchorName + n)) { + n = (n || 1) + 1; + } + this._setAnchorName(anchorName + n); + } + return `${window.location.pathname}#${this.$target[0].id}`; + }, + /** + * Creates a safe id/anchor from text. + * + * @private + * @param {string} text + * @returns {string} + */ + _text2Anchor: function (text) { + return encodeURIComponent(text.trim().replace(/\s+/g, '-')); + }, +}); + +/** + * Controls box properties. + */ +options.registry.Box = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for parameters + */ + setShadow(previewMode, widgetValue, params) { + this.$target.toggleClass(params.shadowClass, !!widgetValue); + const defaultShadow = this._getDefaultShadow(widgetValue, params.shadowClass); + this.$target[0].style.setProperty('box-shadow', defaultShadow, 'important'); + if (widgetValue === 'outset') { + // In this case, the shadowClass is enough + this.$target[0].style.setProperty('box-shadow', ''); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState(methodName, params) { + if (methodName === 'setShadow') { + const shadowValue = this.$target.css('box-shadow'); + if (!shadowValue || shadowValue === 'none') { + return ''; + } + return this.$target.css('box-shadow').includes('inset') ? 'inset' : 'outset'; + } + return this._super(...arguments); + }, + /** + * @override + */ + async _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'fake_inset_shadow_opt') { + return false; + } + return this._super(...arguments); + }, + /** + * @private + * @param {string} type + * @param {string} shadowClass + * @returns {string} + */ + _getDefaultShadow(type, shadowClass) { + const el = document.createElement('div'); + if (type) { + el.classList.add(shadowClass); + } + document.body.appendChild(el); + switch (type) { + case 'outset': { + return $(el).css('box-shadow'); + } + case 'inset': { + return $(el).css('box-shadow') + ' inset'; + } + } + el.remove(); + return ''; + } +}); + +options.registry.HeaderBox = options.registry.Box.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @override + */ + async selectStyle(previewMode, widgetValue, params) { + if ((params.variable || params.color) + && ['border-width', 'border-style', 'border-color', 'border-radius', 'box-shadow'].includes(params.cssProperty)) { + if (previewMode) { + return; + } + if (params.cssProperty === 'border-color') { + return this.customizeWebsiteColor(previewMode, widgetValue, params); + } + return this.customizeWebsiteVariable(previewMode, widgetValue, params); + } + return this._super(...arguments); + }, + /** + * @override + */ + async setShadow(previewMode, widgetValue, params) { + if (params.variable) { + if (previewMode) { + return; + } + const defaultShadow = this._getDefaultShadow(widgetValue, params.shadowClass); + return this.customizeWebsiteVariable(previewMode, defaultShadow || 'none', params); + } + return this._super(...arguments); + }, +}); + +options.registry.CookiesBar = options.registry.SnippetPopup.extend({ + xmlDependencies: (options.registry.SnippetPopup.prototype.xmlDependencies || []).concat( + ['/website/static/src/xml/website.cookies_bar.xml'] + ), + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Change the cookies bar layout. + * + * @see this.selectClass for parameters + */ + selectLayout: function (previewMode, widgetValue, params) { + let websiteId; + this.trigger_up('context_get', { + callback: function (ctx) { + websiteId = ctx['website_id']; + }, + }); + + const $template = $(qweb.render(`website.cookies_bar.${widgetValue}`, { + websiteId: websiteId, + })); + + const $content = this.$target.find('.modal-content'); + const selectorsToKeep = [ + '.o_cookies_bar_text_button', + '.o_cookies_bar_text_policy', + '.o_cookies_bar_text_title', + '.o_cookies_bar_text_primary', + '.o_cookies_bar_text_secondary', + ]; + + if (this.$savedSelectors === undefined) { + this.$savedSelectors = []; + } + + for (const selector of selectorsToKeep) { + const $currentLayoutEls = $content.find(selector).contents(); + const $newLayoutEl = $template.find(selector); + if ($currentLayoutEls.length) { + // save value before change, eg 'title' is not inside 'discrete' template + // but we want to preserve it in case of select another layout later + this.$savedSelectors[selector] = $currentLayoutEls; + } + const $savedSelector = this.$savedSelectors[selector]; + if ($newLayoutEl.length && $savedSelector && $savedSelector.length) { + $newLayoutEl.empty().append($savedSelector); + } + } + + $content.empty().append($template); + }, +}); + +/** + * Allows edition of 'cover_properties' in website models which have such + * fields (blogs, posts, events, ...). + */ +options.registry.CoverProperties = options.Class.extend({ + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + + this.$image = this.$target.find('.o_record_cover_image'); + this.$filter = this.$target.find('.o_record_cover_filter'); + }, + /** + * @override + */ + start: function () { + this.$filterValueOpts = this.$el.find('[data-filter-value]'); + + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Handles a background change. + * + * @see this.selectClass for parameters + */ + background: async function (previewMode, widgetValue, params) { + if (widgetValue === '') { + this.$image.css('background-image', ''); + this.$target.removeClass('o_record_has_cover'); + } else { + this.$image.css('background-image', `url('${widgetValue}')`); + this.$target.addClass('o_record_has_cover'); + const $defaultSizeBtn = this.$el.find('.o_record_cover_opt_size_default'); + $defaultSizeBtn.click(); + $defaultSizeBtn.closest('we-select').click(); + } + }, + /** + * @see this.selectClass for parameters + */ + filterValue: function (previewMode, widgetValue, params) { + this.$filter.css('opacity', widgetValue || 0); + this.$filter.toggleClass('oe_black', parseFloat(widgetValue) !== 0); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @private + */ + updateUI: async function () { + await this._super(...arguments); + + // TODO: `o_record_has_cover` should be handled using model field, not + // resize_class to avoid all of this. + let coverClass = this.$el.find('[data-cover-opt-name="size"] we-button.active').data('selectClass') || ''; + const bg = this.$image.css('background-image'); + if (bg && bg !== 'none') { + coverClass += " o_record_has_cover"; + } + // Update saving dataset + this.$target[0].dataset.coverClass = coverClass; + this.$target[0].dataset.textAlignClass = this.$el.find('[data-cover-opt-name="text_align"] we-button.active').data('selectClass') || ''; + this.$target[0].dataset.filterValue = this.$filterValueOpts.filter('.active').data('filterValue') || 0.0; + let colorPickerWidget = null; + this.trigger_up('user_value_widget_request', { + name: 'bg_color_opt', + onSuccess: _widget => colorPickerWidget = _widget, + }); + const color = colorPickerWidget._value; + const isCSSColor = ColorpickerWidget.isCSSColor(color); + this.$target[0].dataset.bgColorClass = isCSSColor ? '' : weUtils.computeColorClasses([color])[0]; + this.$target[0].dataset.bgColorStyle = isCSSColor ? `background-color: ${color};` : ''; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'filterValue': { + return parseFloat(this.$filter.css('opacity')).toFixed(1); + } + case 'background': { + const background = this.$image.css('background-image'); + if (background && background !== 'none') { + return background.match(/^url\(["']?(.+?)["']?\)$/)[1]; + } + return ''; + } + } + return this._super(...arguments); + }, + /** + * @override + */ + _computeWidgetVisibility: function (widgetName, params) { + if (params.coverOptName) { + return this.$target.data(`use_${params.coverOptName}`) === 'True'; + } + return this._super(...arguments); + }, +}); + +options.registry.ContainerWidth = options.Class.extend({ + /** + * @override + */ + cleanForSave: function () { + this.$target.removeClass('o_container_preview'); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @override + */ + selectClass: async function (previewMode, widgetValue, params) { + await this._super(...arguments); + if (previewMode === 'reset') { + this.$target.removeClass('o_container_preview'); + } else if (previewMode) { + this.$target.addClass('o_container_preview'); + } + }, +}); + +/** + * Allows snippets to be moved before the preceding element or after the following. + */ +options.registry.SnippetMove = options.Class.extend({ + /** + * @override + */ + start: function () { + var $buttons = this.$el.find('we-button'); + var $overlayArea = this.$overlay.find('.o_overlay_move_options'); + $overlayArea.prepend($buttons[0]); + $overlayArea.append($buttons[1]); + + return this._super(...arguments); + }, + /** + * @override + */ + onFocus: function () { + // TODO improve this: hack to hide options section if snippet move is + // the only one. + const $allOptions = this.$el.parent(); + if ($allOptions.find('we-customizeblock-option').length <= 1) { + $allOptions.addClass('d-none'); + } + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Moves the snippet around. + * + * @see this.selectClass for parameters + */ + moveSnippet: function (previewMode, widgetValue, params) { + const isNavItem = this.$target[0].classList.contains('nav-item'); + const $tabPane = isNavItem ? $(this.$target.find('.nav-link')[0].hash) : null; + switch (widgetValue) { + case 'prev': + this.$target.prev().before(this.$target); + if (isNavItem) { + $tabPane.prev().before($tabPane); + } + break; + case 'next': + this.$target.next().after(this.$target); + if (isNavItem) { + $tabPane.next().after($tabPane); + } + break; + } + if (params.name === 'move_up_opt' || params.name === 'move_down_opt') { + dom.scrollTo(this.$target[0], { + extraOffset: 50, + easing: 'linear', + duration: 550, + }); + } + }, +}); + +options.registry.ScrollButton = options.Class.extend({ + /** + * @override + */ + start: async function () { + await this._super(...arguments); + this.$button = this.$('.o_scroll_button'); + }, + /** + * Removes button if the option is not displayed (for example in "fit + * content" height). + * + * @override + */ + updateUIVisibility: async function () { + await this._super(...arguments); + if (this.$button.length && this.el.offsetParent === null) { + this.$button.detach(); + } + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Toggles the scroll down button. + */ + toggleButton: function (previewMode, widgetValue, params) { + if (widgetValue) { + if (!this.$button.length) { + const anchor = document.createElement('a'); + anchor.classList.add( + 'o_scroll_button', + 'mb-3', + 'rounded-circle', + 'align-items-center', + 'justify-content-center', + 'mx-auto', + 'bg-primary', + ); + anchor.href = '#'; + anchor.contentEditable = "false"; + anchor.title = _t("Scroll down to next section"); + const arrow = document.createElement('i'); + arrow.classList.add('fa', 'fa-angle-down', 'fa-3x'); + anchor.appendChild(arrow); + this.$button = $(anchor); + } + this.$target.append(this.$button); + } else { + this.$button.detach(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'toggleButton': + return !!this.$button.parent().length; + } + return this._super(...arguments); + }, +}); + +return { + UrlPickerUserValueWidget: UrlPickerUserValueWidget, + FontFamilyPickerUserValueWidget: FontFamilyPickerUserValueWidget, +}; +}); diff --git a/addons/website/static/src/js/editor/widget_link.js b/addons/website/static/src/js/editor/widget_link.js new file mode 100644 index 00000000..de3c1d9c --- /dev/null +++ b/addons/website/static/src/js/editor/widget_link.js @@ -0,0 +1,104 @@ +odoo.define('website.editor.link', function (require) { +'use strict'; + +var weWidgets = require('wysiwyg.widgets'); +var wUtils = require('website.utils'); + +weWidgets.LinkDialog.include({ + xmlDependencies: (weWidgets.LinkDialog.prototype.xmlDependencies || []).concat( + ['/website/static/src/xml/website.editor.xml'] + ), + events: _.extend({}, weWidgets.LinkDialog.prototype.events || {}, { + 'change select[name="link_anchor"]': '_onAnchorChange', + 'input input[name="url"]': '_onURLInput', + }), + custom_events: _.extend({}, weWidgets.LinkDialog.prototype.custom_events || {}, { + website_url_chosen: '_onAutocompleteClose', + }), + LINK_DEBOUNCE: 1000, + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + this._adaptPageAnchor = _.debounce(this._adaptPageAnchor, this.LINK_DEBOUNCE); + }, + /** + * Allows the URL input to propose existing website pages. + * + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + wUtils.autocompleteWithPages(this, this.$('input[name="url"]')); + this.opened(this._adaptPageAnchor.bind(this)); + return def; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptPageAnchor: function () { + var urlInputValue = this.$('input[name="url"]').val(); + var $pageAnchor = this.$('.o_link_dialog_page_anchor'); + var isFromWebsite = urlInputValue[0] === '/'; + var $selectMenu = this.$('select[name="link_anchor"]'); + var $anchorsLoading = this.$('.o_anchors_loading'); + + if ($selectMenu.data("anchor-for") !== urlInputValue) { // avoid useless query + $anchorsLoading.removeClass('d-none'); + $pageAnchor.toggleClass('d-none', !isFromWebsite); + $selectMenu.empty(); + wUtils.loadAnchors(urlInputValue).then(function (anchors) { + _.each(anchors, function (anchor) { + $selectMenu.append($('<option>', {text: anchor})); + }); + always(); + }).guardedCatch(always); + } else { + always(); + } + + function always() { + $anchorsLoading.addClass('d-none'); + $selectMenu.prop("selectedIndex", -1); + } + $selectMenu.data("anchor-for", urlInputValue); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAutocompleteClose: function () { + this._onURLInput(); + }, + /** + * @private + */ + _onAnchorChange: function () { + var anchorValue = this.$('[name="link_anchor"]').val(); + var $urlInput = this.$('[name="url"]'); + var urlInputValue = $urlInput.val(); + if (urlInputValue.indexOf('#') > -1) { + urlInputValue = urlInputValue.substr(0, urlInputValue.indexOf('#')); + } + $urlInput.val(urlInputValue + anchorValue); + }, + /** + * @override + */ + _onURLInput: function () { + this._super.apply(this, arguments); + this._adaptPageAnchor(); + }, +}); +}); diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone.js b/addons/website/static/src/js/editor/wysiwyg_multizone.js new file mode 100644 index 00000000..596d40db --- /dev/null +++ b/addons/website/static/src/js/editor/wysiwyg_multizone.js @@ -0,0 +1,286 @@ +odoo.define('web_editor.wysiwyg.multizone', function (require) { +'use strict'; + +var Wysiwyg = require('web_editor.wysiwyg'); +var snippetsEditor = require('web_editor.snippet.editor'); + +/** + * Show/hide the dropdowns associated to the given toggles and allows to wait + * for when it is fully shown/hidden. + * + * Note: this also takes care of the fact the 'toggle' method of bootstrap does + * not properly work in all cases. + * + * @param {jQuery} $toggles + * @param {boolean} [show] + * @returns {Promise<jQuery>} + */ +function toggleDropdown($toggles, show) { + return Promise.all(_.map($toggles, toggle => { + var $toggle = $(toggle); + var $dropdown = $toggle.parent(); + var shown = $dropdown.hasClass('show'); + if (shown === show) { + return; + } + var toShow = !shown; + return new Promise(resolve => { + $dropdown.one( + toShow ? 'shown.bs.dropdown' : 'hidden.bs.dropdown', + () => resolve() + ); + $toggle.dropdown(toShow ? 'show' : 'hide'); + }); + })).then(() => $toggles); +} + +/** + * HtmlEditor + * Intended to edit HTML content. This widget uses the Wysiwyg editor + * improved by odoo. + * + * class editable: o_editable + * class non editable: o_not_editable + * + */ +var WysiwygMultizone = Wysiwyg.extend({ + /** + * @override + */ + start: function () { + var self = this; + this.options.toolbarHandler = $('#web_editor-top-edit'); + this.options.saveElement = function ($el, context, withLang) { + var outerHTML = this._getEscapedElement($el).prop('outerHTML'); + return self._saveElement(outerHTML, self.options.recordInfo, $el[0]); + }; + + // Mega menu initialization: handle dropdown openings by hand + var $megaMenuToggles = this.$('.o_mega_menu_toggle'); + $megaMenuToggles.removeAttr('data-toggle').dropdown('dispose'); + $megaMenuToggles.on('click.wysiwyg_multizone', ev => { + var $toggle = $(ev.currentTarget); + + // Each time we toggle a dropdown, we will destroy the dropdown + // behavior afterwards to keep manual control of it + var dispose = ($els => $els.dropdown('dispose')); + + // First hide all other mega menus + toggleDropdown($megaMenuToggles.not($toggle), false).then(dispose); + + // Then toggle the clicked one + toggleDropdown($toggle) + .then(dispose) + .then($el => { + var isShown = $el.parent().hasClass('show'); + this.editor.snippetsMenu.toggleMegaMenuSnippets(isShown); + }); + }); + + // Ensure :blank oe_structure elements are in fact empty as ':blank' + // does not really work with all browsers. + for (const el of this.$('.oe_structure')) { + if (!el.innerHTML.trim()) { + el.innerHTML = ''; + } + } + + // TODO remove this code in master by migrating users who did not + // receive the XML change about the 'oe_structure_solo' class (the + // header original XML is now correct but we changed specs after + // release to not allow multi snippets drop zones in the header). + const $headerZones = this._getEditableArea().filter((i, el) => el.closest('header#top') !== null); + // oe_structure_multi to ease custo in stable + const selector = '.oe_structure[id*="oe_structure"]:not(.oe_structure_multi)'; + $headerZones.find(selector).addBack(selector).addClass('oe_structure_solo'); + + return this._super.apply(this, arguments).then(() => { + // Showing Mega Menu snippets if one dropdown is already opened + if (this.$('.o_mega_menu').hasClass('show')) { + this.editor.snippetsMenu.toggleMegaMenuSnippets(true); + } + }); + }, + /** + * @override + * @returns {Promise} + */ + save: function () { + if (this.isDirty()) { + return this._restoreMegaMenus() + .then(() => this.editor.save(false)) + .then(() => ({isDirty: true})); + } else { + return {isDirty: false}; + } + }, + /** + * @override + */ + destroy: function () { + this._restoreMegaMenus(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _getEditableArea: function () { + return $(':o_editable'); + }, + /** + * @private + * @param {HTMLElement} editable + */ + _saveCoverProperties: function (editable) { + var el = editable.closest('.o_record_cover_container'); + if (!el) { + return; + } + + var resModel = el.dataset.resModel; + var resID = parseInt(el.dataset.resId); + if (!resModel || !resID) { + throw new Error('There should be a model and id associated to the cover'); + } + + this.__savedCovers = this.__savedCovers || {}; + this.__savedCovers[resModel] = this.__savedCovers[resModel] || []; + + if (this.__savedCovers[resModel].includes(resID)) { + return; + } + this.__savedCovers[resModel].push(resID); + + var cssBgImage = $(el.querySelector('.o_record_cover_image')).css('background-image'); + var coverProps = { + 'background-image': cssBgImage.replace(/"/g, '').replace(window.location.protocol + "//" + window.location.host, ''), + 'background_color_class': el.dataset.bgColorClass, + 'background_color_style': el.dataset.bgColorStyle, + 'opacity': el.dataset.filterValue, + 'resize_class': el.dataset.coverClass, + 'text_align_class': el.dataset.textAlignClass, + }; + + return this._rpc({ + model: resModel, + method: 'write', + args: [ + resID, + {'cover_properties': JSON.stringify(coverProps)} + ], + }); + }, + /** + * Saves one (dirty) element of the page. + * + * @private + * @param {jQuery} $el - the element to save + * @param {Object} context - the context to use for the saving rpc + * @param {boolean} [withLang=false] + * false if the lang must be omitted in the context (saving "master" + * page element) + */ + _saveElement: function (outerHTML, recordInfo, editable) { + var promises = []; + + var $el = $(editable); + + // Saving a view content + var viewID = $el.data('oe-id'); + if (viewID) { + promises.push(this._rpc({ + model: 'ir.ui.view', + method: 'save', + args: [ + viewID, + outerHTML, + $el.data('oe-xpath') || null, + ], + context: recordInfo.context, + })); + } + + // Saving mega menu options + if ($el.data('oe-field') === 'mega_menu_content') { + // On top of saving the mega menu content like any other field + // content, we must save the custom classes that were set on the + // menu itself. + // FIXME normally removing the 'show' class should not be necessary here + // TODO check that editor classes are removed here as well + var classes = _.without($el.attr('class').split(' '), 'dropdown-menu', 'o_mega_menu', 'show'); + promises.push(this._rpc({ + model: 'website.menu', + method: 'write', + args: [ + [parseInt($el.data('oe-id'))], + { + 'mega_menu_classes': classes.join(' '), + }, + ], + })); + } + + // Saving cover properties on related model if any + var prom = this._saveCoverProperties(editable); + if (prom) { + promises.push(prom); + } + + return Promise.all(promises); + }, + /** + * Restores mega menu behaviors and closes them (important to do before + * saving otherwise they would be saved opened). + * + * @private + * @returns {Promise} + */ + _restoreMegaMenus: function () { + var $megaMenuToggles = this.$('.o_mega_menu_toggle'); + $megaMenuToggles.off('.wysiwyg_multizone') + .attr('data-toggle', 'dropdown') + .dropdown({}); + return toggleDropdown($megaMenuToggles, false); + }, +}); + +snippetsEditor.Class.include({ + /** + * @private + * @param {boolean} show + */ + toggleMegaMenuSnippets: function (show) { + setTimeout(() => this._activateSnippet(false)); + this._showMegaMenuSnippets = show; + this._filterSnippets(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _filterSnippets(search) { + this._super(...arguments); + if (!this._showMegaMenuSnippets) { + this.el.querySelector('#snippet_mega_menu').classList.add('d-none'); + } + }, + /** + * @override + */ + _insertDropzone: function ($hook) { + var $hookParent = $hook.parent(); + var $dropzone = this._super(...arguments); + $dropzone.attr('data-editor-message', $hookParent.attr('data-editor-message')); + $dropzone.attr('data-editor-sub-message', $hookParent.attr('data-editor-sub-message')); + return $dropzone; + }, +}); + +return WysiwygMultizone; +}); diff --git a/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js new file mode 100644 index 00000000..978ac4d9 --- /dev/null +++ b/addons/website/static/src/js/editor/wysiwyg_multizone_translate.js @@ -0,0 +1,301 @@ +odoo.define('web_editor.wysiwyg.multizone.translate', function (require) { +'use strict'; + +var core = require('web.core'); +var webDialog = require('web.Dialog'); +var WysiwygMultizone = require('web_editor.wysiwyg.multizone'); +var rte = require('web_editor.rte'); +var Dialog = require('wysiwyg.widgets.Dialog'); +var websiteNavbarData = require('website.navbar'); + +var _t = core._t; + + +var RTETranslatorWidget = rte.Class.extend({ + /** + * If the element holds a translation, saves it. Otherwise, fallback to the + * standard saving but with the lang kept. + * + * @override + */ + _saveElement: function ($el, context, withLang) { + var self = this; + if ($el.data('oe-translation-id')) { + return this._rpc({ + model: 'ir.translation', + method: 'save_html', + args: [ + [+$el.data('oe-translation-id')], + this._getEscapedElement($el).html() + ], + context: context, + }); + } + return this._super($el, context, withLang === undefined ? true : withLang); + }, +}); + +var AttributeTranslateDialog = Dialog.extend({ + /** + * @constructor + */ + init: function (parent, options, node) { + this._super(parent, _.extend({ + title: _t("Translate Attribute"), + buttons: [ + {text: _t("Close"), classes: 'btn-primary', click: this.save} + ], + }, options || {})); + this.translation = $(node).data('translation'); + }, + /** + * @override + */ + start: function () { + var $group = $('<div/>', {class: 'form-group'}).appendTo(this.$el); + _.each(this.translation, function (node, attr) { + var $node = $(node); + var $label = $('<label class="col-form-label"></label>').text(attr); + var $input = $('<input class="form-control"/>').val($node.html()); + $input.on('change keyup', function () { + var value = $input.val(); + $node.html(value).trigger('change', node); + $node.data('$node').attr($node.data('attribute'), value).trigger('translate'); + $node.trigger('change'); + }); + $group.append($label).append($input); + }); + return this._super.apply(this, arguments); + } +}); + +var WysiwygTranslate = WysiwygMultizone.extend({ + custom_events: _.extend({}, WysiwygMultizone.prototype.custom_events || {}, { + ready_to_save: '_onSave', + rte_change: '_onChange', + }), + + /** + * @override + * @param {string} options.lang + */ + init: function (parent, options) { + this.lang = options.lang; + options.recordInfo = _.defaults({ + context: {lang: this.lang} + }, options.recordInfo, options); + this._super.apply(this, arguments); + }, + /** + * @override + */ + start: function () { + var self = this; + // Hacky way to keep the top editor toolbar in translate mode for now + this.$webEditorTopEdit = $('<div id="web_editor-top-edit"></div>').prependTo(document.body); + this.options.toolbarHandler = this.$webEditorTopEdit; + this.editor = new (this.Editor)(this, Object.assign({Editor: RTETranslatorWidget}, this.options)); + this.$editor = this.editor.rte.editable(); + var promise = this.editor.prependTo(this.$editor[0].ownerDocument.body); + + return promise.then(function () { + self._relocateEditorBar(); + var attrs = ['placeholder', 'title', 'alt']; + _.each(attrs, function (attr) { + self._getEditableArea().filter('[' + attr + '*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () { + var $node = $(this); + var translation = $node.data('translation') || {}; + var trans = $node.attr(attr); + var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/); + var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_attribute').appendTo('body'); + $trans.data('$node', $node).data('attribute', attr); + + translation[attr] = $trans[0]; + $node.attr(attr, match[2]); + + var select2 = $node.data('select2'); + if (select2) { + select2.blur(); + $node.on('translate', function () { + select2.blur(); + }); + $node = select2.container.find('input'); + } + $node.addClass('o_translatable_attribute').data('translation', translation); + }); + }); + + self.translations = []; + self.$editables_attr = self._getEditableArea().filter('.o_translatable_attribute'); + self.$editables_attribute = $('.o_editable_translatable_attribute'); + + self.$editables_attribute.on('change', function () { + self.trigger_up('rte_change', {target: this}); + }); + + self._markTranslatableNodes(); + }); + }, + /** + * @override + */ + destroy: function () { + this._super(...arguments); + this.$webEditorTopEdit.remove(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @returns {Boolean} + */ + isDirty: function () { + return this._super() || this.$editables_attribute.hasClass('o_dirty'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Return the editable area. + * + * @override + * @returns {JQuery} + */ + _getEditableArea: function () { + var $editables = this._super(); + return $editables.add(this.$editables_attribute); + }, + /** + * Return an object describing the linked record. + * + * @override + * @param {Object} options + * @returns {Object} {res_id, res_model, xpath} + */ + _getRecordInfo: function (options) { + options = options || {}; + var recordInfo = this._super(options); + var $editable = $(options.target).closest(this._getEditableArea()); + if (!$editable.length) { + $editable = $(this._getFocusedEditable()); + } + recordInfo.context.lang = this.lang; + recordInfo.translation_id = $editable.data('oe-translation-id')|0; + return recordInfo; + }, + /** + * @override + * @returns {Object} the summernote configuration + */ + _editorOptions: function () { + var options = this._super(); + options.toolbar = [ + // todo: hide this feature for field (data-oe-model) + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + // keep every time + ['history', ['undo', 'redo']], + ]; + return options; + }, + /** + * Called when text is edited -> make sure text is not messed up and mark + * the element as dirty. + * + * @override + * @param {Jquery Event} [ev] + */ + _onChange: function (ev) { + var $node = $(ev.data.target); + if (!$node.length) { + return; + } + $node.find('div,p').each(function () { // remove P,DIV elements which might have been inserted because of copy-paste + var $p = $(this); + $p.after($p.html()).remove(); + }); + var trans = this._getTranlationObject($node[0]); + $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' ')); + }, + /** + * Returns a translation object. + * + * @private + * @param {Node} node + * @returns {Object} + */ + _getTranlationObject: function (node) { + var $node = $(node); + var id = +$node.data('oe-translation-id'); + if (!id) { + id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field'); + } + var trans = _.find(this.translations, function (trans) { + return trans.id === id; + }); + if (!trans) { + this.translations.push(trans = {'id': id}); + } + return trans; + }, + /** + * @private + */ + _markTranslatableNodes: function () { + var self = this; + this._getEditableArea().each(function () { + var $node = $(this); + var trans = self._getTranlationObject(this); + trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' '); + }); + this._getEditableArea().prependEvent('click.translator', function (ev) { + if (ev.ctrlKey || !$(ev.target).is(':o_editable')) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + }); + + // attributes + + this.$editables_attr.each(function () { + var $node = $(this); + var translation = $node.data('translation'); + _.each(translation, function (node, attr) { + var trans = self._getTranlationObject(node); + trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' '); + $node.attr('data-oe-translation-state', (trans.state || 'to_translate')); + }); + }); + + this.$editables_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) { + if (ev.ctrlKey) { + return; + } + ev.preventDefault(); + ev.stopPropagation(); + if (ev.type !== 'mousedown') { + return; + } + + new AttributeTranslateDialog(self, {}, ev.target).open(); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onSave: function (ev) { + ev.stopPropagation(); + }, +}); + +return WysiwygTranslate; +}); diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js new file mode 100644 index 00000000..d2dff980 --- /dev/null +++ b/addons/website/static/src/js/menu/content.js @@ -0,0 +1,1129 @@ +odoo.define('website.contentMenu', function (require) { +'use strict'; + +var Class = require('web.Class'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var time = require('web.time'); +var weWidgets = require('wysiwyg.widgets'); +var websiteNavbarData = require('website.navbar'); +var websiteRootData = require('website.root'); +var Widget = require('web.Widget'); + +var _t = core._t; +var qweb = core.qweb; + +var PagePropertiesDialog = weWidgets.Dialog.extend({ + template: 'website.pagesMenu.page_info', + xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.pageProperties.xml'] + ), + events: _.extend({}, weWidgets.Dialog.prototype.events, { + 'keyup input#page_name': '_onNameChanged', + 'keyup input#page_url': '_onUrlChanged', + 'change input#create_redirect': '_onCreateRedirectChanged', + 'click input#visibility_password': '_onPasswordClicked', + 'change input#visibility_password': '_onPasswordChanged', + 'change select#visibility': '_onVisibilityChanged', + 'error.datetimepicker': '_onDateTimePickerError', + }), + + /** + * @constructor + * @override + */ + init: function (parent, page_id, options) { + var self = this; + var serverUrl = window.location.origin + '/'; + var length_url = serverUrl.length; + var serverUrlTrunc = serverUrl; + if (length_url > 30) { + serverUrlTrunc = serverUrl.slice(0,14) + '..' + serverUrl.slice(-14); + } + this.serverUrl = serverUrl; + this.serverUrlTrunc = serverUrlTrunc; + this.current_page_url = window.location.pathname; + this.page_id = page_id; + + var buttons = [ + {text: _t("Save"), classes: 'btn-primary', click: this.save}, + {text: _t("Discard"), classes: 'mr-auto', close: true}, + ]; + if (options.fromPageManagement) { + buttons.push({ + text: _t("Go To Page"), + icon: 'fa-globe', + classes: 'btn-link', + click: function (e) { + window.location.href = '/' + self.page.url; + }, + }); + } + buttons.push({ + text: _t("Duplicate Page"), + icon: 'fa-clone', + classes: 'btn-link', + click: function (e) { + // modal('hide') will break the rpc, so hide manually + this.$el.closest('.modal').addClass('d-none'); + _clonePage.call(this, self.page_id); + }, + }); + buttons.push({ + text: _t("Delete Page"), + icon: 'fa-trash', + classes: 'btn-link', + click: function (e) { + _deletePage.call(this, self.page_id, options.fromPageManagement); + }, + }); + this._super(parent, _.extend({}, { + title: _t("Page Properties"), + size: 'medium', + buttons: buttons, + }, options || {})); + }, + /** + * @override + */ + willStart: function () { + var defs = [this._super.apply(this, arguments)]; + var self = this; + + defs.push(this._rpc({ + model: 'website.page', + method: 'get_page_properties', + args: [this.page_id], + }).then(function (page) { + page.url = _.str.startsWith(page.url, '/') ? page.url.substring(1) : page.url; + page.hasSingleGroup = page.group_id !== undefined; + self.page = page; + })); + + return Promise.all(defs); + }, + /** + * @override + */ + start: function () { + var self = this; + + var defs = [this._super.apply(this, arguments)]; + + this.$('.ask_for_redirect').addClass('d-none'); + this.$('.redirect_type').addClass('d-none'); + this.$('.warn_about_call').addClass('d-none'); + if (this.page.visibility !== 'password') { + this.$('.show_visibility_password').addClass('d-none'); + } + if (this.page.visibility !== 'restricted_group') { + this.$('.show_group_id').addClass('d-none'); + } + this.autocompleteWithGroups(this.$('#group_id')); + + defs.push(this._getPageDependencies(this.page_id) + .then(function (dependencies) { + var dep_text = []; + _.each(dependencies, function (value, index) { + if (value.length > 0) { + dep_text.push(value.length + ' ' + index.toLowerCase()); + } + }); + dep_text = dep_text.join(', '); + self.$('#dependencies_redirect').html(qweb.render('website.show_page_dependencies', { dependencies: dependencies, dep_text: dep_text })); + self.$('#dependencies_redirect [data-toggle="popover"]').popover({ + container: 'body', + }); + })); + + defs.push(this._getSupportedMimetype() + .then(function (mimetypes) { + self.supportedMimetype = mimetypes; + })); + + defs.push(this._getPageKeyDependencies(this.page_id) + .then(function (dependencies) { + var dep_text = []; + _.each(dependencies, function (value, index) { + if (value.length > 0) { + dep_text.push(value.length + ' ' + index.toLowerCase()); + } + }); + dep_text = dep_text.join(', '); + self.$('.warn_about_call').html(qweb.render('website.show_page_key_dependencies', {dependencies: dependencies, dep_text: dep_text})); + self.$('.warn_about_call [data-toggle="popover"]').popover({ + container: 'body', + }); + })); + + defs.push(this._rpc({model: 'res.users', + method: 'has_group', + args: ['website.group_multi_website']}) + .then(function (has_group) { + if (!has_group) { + self.$('#website_restriction').addClass('hidden'); + } + })); + + var datepickersOptions = { + minDate: moment({ y: 1000 }), + maxDate: moment().add(200, 'y'), + calendarWeeks: true, + icons : { + time: 'fa fa-clock-o', + date: 'fa fa-calendar', + next: 'fa fa-chevron-right', + previous: 'fa fa-chevron-left', + up: 'fa fa-chevron-up', + down: 'fa fa-chevron-down', + }, + locale : moment.locale(), + format : time.getLangDatetimeFormat(), + widgetPositioning : { + horizontal: 'auto', + vertical: 'top', + }, + widgetParent: 'body', + }; + if (this.page.date_publish) { + datepickersOptions.defaultDate = time.str_to_datetime(this.page.date_publish); + } + this.$('#date_publish_container').datetimepicker(datepickersOptions); + return Promise.all(defs); + }, + /** + * @override + */ + destroy: function () { + $('.popover').popover('hide'); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + save: function (data) { + var self = this; + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + var url = this.$('#page_url').val(); + + var $datePublish = this.$("#date_publish"); + $datePublish.closest(".form-group").removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + var datePublish = $datePublish.val(); + if (datePublish !== "") { + datePublish = this._parse_date(datePublish); + if (!datePublish) { + $datePublish.closest(".form-group").addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + return; + } + } + var params = { + id: this.page.id, + name: this.$('#page_name').val(), + // Replace duplicate following '/' by only one '/' + url: url.replace(/\/{2,}/g, '/'), + is_menu: this.$('#is_menu').prop('checked'), + is_homepage: this.$('#is_homepage').prop('checked'), + website_published: this.$('#is_published').prop('checked'), + create_redirect: this.$('#create_redirect').prop('checked'), + redirect_type: this.$('#redirect_type').val(), + website_indexed: this.$('#is_indexed').prop('checked'), + visibility: this.$('#visibility').val(), + date_publish: datePublish, + }; + if (this.page.hasSingleGroup && this.$('#visibility').val() === 'restricted_group') { + params['group_id'] = this.$('#group_id').data('group-id'); + } + if (this.$('#visibility').val() === 'password') { + var field_pwd = $('#visibility_password'); + if (!field_pwd.get(0).reportValidity()) { + return; + } + if (field_pwd.data('dirty')) { + params['visibility_pwd'] = field_pwd.val(); + } + } + + this._rpc({ + model: 'website.page', + method: 'save_page_info', + args: [[context.website_id], params], + }).then(function (url) { + // If from page manager: reload url, if from page itself: go to + // (possibly) new url + var mo; + self.trigger_up('main_object_request', { + callback: function (value) { + mo = value; + }, + }); + if (mo.model === 'website.page') { + window.location.href = url.toLowerCase(); + } else { + window.location.reload(true); + } + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Retrieves the page URL dependencies for the given object id. + * + * @private + * @param {integer} moID + * @returns {Promise<Array>} + */ + _getPageDependencies: function (moID) { + return this._rpc({ + model: 'website', + method: 'page_search_dependencies', + args: [moID], + }); + }, + /** + * Retrieves the page's key dependencies for the given object id. + * + * @private + * @param {integer} moID + * @returns {Promise<Array>} + */ + _getPageKeyDependencies: function (moID) { + return this._rpc({ + model: 'website', + method: 'page_search_key_dependencies', + args: [moID], + }); + }, + /** + * Retrieves supported mimtype + * + * @private + * @returns {Promise<Array>} + */ + _getSupportedMimetype: function () { + return this._rpc({ + model: 'website', + method: 'guess_mimetype', + }); + }, + /** + * Returns information about the page main object. + * + * @private + * @returns {Object} model and id + */ + _getMainObject: function () { + var repr = $('html').data('main-object'); + var m = repr.match(/(.+)\((\d+),(.*)\)/); + return { + model: m[1], + id: m[2] | 0, + }; + }, + /** + * Converts a string representing the browser datetime + * (exemple: Albanian: '2018-Qer-22 15.12.35.') + * to a string representing UTC in Odoo's datetime string format + * (exemple: '2018-04-22 13:12:35'). + * + * The time zone of the datetime string is assumed to be the one of the + * browser and it will be converted to UTC (standard for Odoo). + * + * @private + * @param {String} value A string representing a datetime. + * @returns {String|false} A string representing an UTC datetime if the given value is valid, false otherwise. + */ + _parse_date: function (value) { + var datetime = moment(value, time.getLangDatetimeFormat(), true); + if (datetime.isValid()) { + return time.datetime_to_str(datetime.toDate()); + } + else { + return false; + } + }, + /** + * Allows the given input to propose existing groups. + * + * @param {jQuery} $input + */ + autocompleteWithGroups: function ($input) { + $input.autocomplete({ + source: (request, response) => { + return this._rpc({ + model: 'res.groups', + method: 'search_read', + args: [[['name', 'ilike', request.term]], ['display_name']], + kwargs: { + limit: 15, + }, + }).then(founds => { + founds = founds.map(g => ({'id': g['id'], 'label': g['display_name']})); + response(founds); + }); + }, + change: (ev, ui) => { + var $target = $(ev.target); + if (!ui.item) { + $target.val(""); + $target.removeData('group-id'); + } else { + $target.data('group-id', ui.item.id); + } + }, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onUrlChanged: function () { + var url = this.$('input#page_url').val(); + this.$('.ask_for_redirect').toggleClass('d-none', url === this.page.url); + }, + /** + * @private + */ + _onNameChanged: function () { + var name = this.$('input#page_name').val(); + // If the file type is a supported mimetype, check if it is t-called. + // If so, warn user. Note: different from page_search_dependencies which + // check only for url and not key + var ext = '.' + this.page.name.split('.').pop(); + if (ext in this.supportedMimetype && ext !== '.html') { + this.$('.warn_about_call').toggleClass('d-none', name === this.page.name); + } + }, + /** + * @private + */ + _onCreateRedirectChanged: function () { + var createRedirect = this.$('input#create_redirect').prop('checked'); + this.$('.redirect_type').toggleClass('d-none', !createRedirect); + }, + /** + * @private + */ + _onVisibilityChanged: function (ev) { + this.$('.show_visibility_password').toggleClass('d-none', ev.target.value !== 'password'); + this.$('.show_group_id').toggleClass('d-none', ev.target.value !== 'restricted_group'); + this.$('#visibility_password').attr('required', ev.target.value === 'password'); + }, + /** + * Library clears the wrong date format so just ignore error + * + * @private + */ + _onDateTimePickerError: function (ev) { + return false; + }, + /** + * @private + */ + _onPasswordClicked: function (ev) { + ev.target.value = ''; + this._onPasswordChanged(); + }, + /** + * @private + */ + _onPasswordChanged: function () { + this.$('#visibility_password').data('dirty', 1); + }, +}); + +var MenuEntryDialog = weWidgets.LinkDialog.extend({ + xmlDependencies: weWidgets.LinkDialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.contentMenu.xml'] + ), + + /** + * @constructor + */ + init: function (parent, options, editable, data) { + this._super(parent, _.extend({ + title: _t("Add a menu item"), + }, options || {}), editable, _.extend({ + needLabel: true, + text: data.name || '', + isNewWindow: data.new_window, + }, data || {})); + + this.menuType = data.menuType; + }, + /** + * @override + */ + start: function () { + // Remove style related elements + this.$('.o_link_dialog_preview').remove(); + this.$('input[name="is_new_window"], .link-style').closest('.form-group').remove(); + this.$modal.find('.modal-lg').removeClass('modal-lg'); + this.$('form.col-lg-8').removeClass('col-lg-8').addClass('col-12'); + + // Adapt URL label + this.$('label[for="o_link_dialog_label_input"]').text(_t("Menu Label")); + + // Auto add '#' URL and hide the input if for mega menu + if (this.menuType === 'mega') { + var $url = this.$('input[name="url"]'); + $url.val('#').trigger('change'); + $url.closest('.form-group').addClass('d-none'); + } + + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + save: function () { + var $e = this.$('#o_link_dialog_label_input'); + if (!$e.val() || !$e[0].checkValidity()) { + $e.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + $e.focus(); + return; + } + return this._super.apply(this, arguments); + }, +}); + +var SelectEditMenuDialog = weWidgets.Dialog.extend({ + template: 'website.contentMenu.dialog.select', + xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.contentMenu.xml'] + ), + + /** + * @constructor + * @override + */ + init: function (parent, options) { + var self = this; + self.roots = [{id: null, name: _t("Top Menu")}]; + $('[data-content_menu_id]').each(function () { + // Remove name fallback in master + self.roots.push({id: $(this).data('content_menu_id'), name: $(this).attr('name') || $(this).data('menu_name')}); + }); + this._super(parent, _.extend({}, { + title: _t("Select a Menu"), + save_text: _t("Continue") + }, options || {})); + }, + /** + * @override + */ + save: function () { + this.final_data = parseInt(this.$el.find('select').val() || null); + this._super.apply(this, arguments); + }, +}); + +var EditMenuDialog = weWidgets.Dialog.extend({ + template: 'website.contentMenu.dialog.edit', + xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.contentMenu.xml'] + ), + events: _.extend({}, weWidgets.Dialog.prototype.events, { + 'click a.js_add_menu': '_onAddMenuButtonClick', + 'click button.js_delete_menu': '_onDeleteMenuButtonClick', + 'click button.js_edit_menu': '_onEditMenuButtonClick', + }), + + /** + * @constructor + * @override + */ + init: function (parent, options, rootID) { + this._super(parent, _.extend({}, { + title: _t("Edit Menu"), + size: 'medium', + }, options || {})); + this.rootID = rootID; + }, + /** + * @override + */ + willStart: function () { + var defs = [this._super.apply(this, arguments)]; + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + defs.push(this._rpc({ + model: 'website.menu', + method: 'get_tree', + args: [context.website_id, this.rootID], + }).then(menu => { + this.menu = menu; + this.rootMenuID = menu.fields['id']; + this.flat = this._flatenize(menu); + this.toDelete = []; + })); + return Promise.all(defs); + }, + /** + * @override + */ + start: function () { + var r = this._super.apply(this, arguments); + this.$('.oe_menu_editor').nestedSortable({ + listType: 'ul', + handle: 'div', + items: 'li', + maxLevels: 2, + toleranceElement: '> div', + forcePlaceholderSize: true, + opacity: 0.6, + placeholder: 'oe_menu_placeholder', + tolerance: 'pointer', + attribute: 'data-menu-id', + expression: '()(.+)', // nestedSortable takes the second match of an expression (*sigh*) + isAllowed: (placeholder, placeholderParent, currentItem) => { + return !placeholderParent + || !currentItem[0].dataset.megaMenu && !placeholderParent[0].dataset.megaMenu; + }, + }); + return r; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + save: function () { + var _super = this._super.bind(this); + var newMenus = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0}); + var levels = []; + var data = []; + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + // Resequence, re-tree and remove useless data + newMenus.forEach(menu => { + if (menu.id) { + levels[menu.depth] = (levels[menu.depth] || 0) + 1; + var menuFields = this.flat[menu.id].fields; + menuFields['sequence'] = levels[menu.depth]; + menuFields['parent_id'] = menu['parent_id'] || this.rootMenuID; + data.push(menuFields); + } + }); + return this._rpc({ + model: 'website.menu', + method: 'save', + args: [ + context.website_id, + { + 'data': data, + 'to_delete': this.toDelete, + } + ], + }).then(function () { + return _super(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Returns a mapping id -> menu item containing all the menu items in the + * given menu hierarchy. + * + * @private + * @param {Object} node + * @param {Object} [_dict] internal use: the mapping being built + * @returns {Object} + */ + _flatenize: function (node, _dict) { + _dict = _dict || {}; + _dict[node.fields['id']] = node; + node.children.forEach(child => { + this._flatenize(child, _dict); + }); + return _dict; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the "add menu" button is clicked -> Opens the appropriate + * dialog to edit this new menu. + * + * @private + * @param {Event} ev + */ + _onAddMenuButtonClick: function (ev) { + var menuType = ev.currentTarget.dataset.type; + var dialog = new MenuEntryDialog(this, {}, null, { + menuType: menuType, + }); + dialog.on('save', this, link => { + var newMenu = { + 'fields': { + 'id': _.uniqueId('new-'), + 'name': _.unescape(link.text), + 'url': link.url, + 'new_window': link.isNewWindow, + 'is_mega_menu': menuType === 'mega', + 'sequence': 0, + 'parent_id': false, + }, + 'children': [], + 'is_homepage': false, + }; + this.flat[newMenu.fields['id']] = newMenu; + this.$('.oe_menu_editor').append( + qweb.render('website.contentMenu.dialog.submenu', {submenu: newMenu}) + ); + }); + dialog.open(); + }, + /** + * Called when the "delete menu" button is clicked -> Deletes this menu. + * + * @private + */ + _onDeleteMenuButtonClick: function (ev) { + var $menu = $(ev.currentTarget).closest('[data-menu-id]'); + var menuID = parseInt($menu.data('menu-id')); + if (menuID) { + this.toDelete.push(menuID); + } + $menu.remove(); + }, + /** + * Called when the "edit menu" button is clicked -> Opens the appropriate + * dialog to edit this menu. + * + * @private + */ + _onEditMenuButtonClick: function (ev) { + var $menu = $(ev.currentTarget).closest('[data-menu-id]'); + var menuID = $menu.data('menu-id'); + var menu = this.flat[menuID]; + if (menu) { + var dialog = new MenuEntryDialog(this, {}, null, _.extend({ + menuType: menu.fields['is_mega_menu'] ? 'mega' : undefined, + }, menu.fields)); + dialog.on('save', this, link => { + _.extend(menu.fields, { + 'name': _.unescape(link.text), + 'url': link.url, + 'new_window': link.isNewWindow, + }); + $menu.find('.js_menu_label').first().text(menu.fields['name']); + }); + dialog.open(); + } else { + Dialog.alert(null, "Could not find menu entry"); + } + }, +}); + +var PageOption = Class.extend({ + /** + * @constructor + * @param {string} name + * the option's name = the field's name in website.page model + * @param {*} value + * @param {function} setValueCallback + * a function which simulates an option's value change without + * asking the server to change it + */ + init: function (name, value, setValueCallback) { + this.name = name; + this.value = value; + this.isDirty = false; + this.setValueCallback = setValueCallback; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Sets the new option's value thanks to the related callback. + * + * @param {*} [value] + * by default: consider the current value is a boolean and toggle it + */ + setValue: function (value) { + if (value === undefined) { + value = !this.value; + } + this.setValueCallback.call(this, value); + this.value = value; + this.isDirty = true; + }, +}); + +var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + xmlDependencies: ['/website/static/src/xml/website.xml'], + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { + edit_menu: '_editMenu', + get_page_option: '_getPageOption', + on_save: '_onSave', + page_properties: '_pageProperties', + toggle_page_option: '_togglePageOption', + }), + pageOptionsSetValueCallbacks: { + header_overlay: function (value) { + $('#wrapwrap').toggleClass('o_header_overlay', value); + }, + header_color: function (value) { + $('#wrapwrap > header').removeClass(this.value) + .addClass(value); + }, + header_visible: function (value) { + $('#wrapwrap > header').toggleClass('d-none o_snippet_invisible', !value); + }, + footer_visible: function (value) { + $('#wrapwrap > footer').toggleClass('d-none o_snippet_invisible', !value); + }, + }, + + /** + * @override + */ + start: function () { + var self = this; + this.pageOptions = {}; + _.each($('.o_page_option_data'), function (el) { + var value = el.value; + if (value === "True") { + value = true; + } else if (value === "False") { + value = false; + } + self.pageOptions[el.name] = new PageOption( + el.name, + value, + self.pageOptionsSetValueCallbacks[el.name] + ); + }); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Asks the user which menu to edit if multiple menus exist on the page. + * Then opens the menu edition dialog. + * Then executes the given callback once the edition is saved, to finally + * reload the page. + * + * @private + * @param {function} [beforeReloadCallback] + * @returns {Promise} + * Unresolved if the menu is edited and saved as the page will be + * reloaded. + * Resolved otherwise. + */ + _editMenu: function (beforeReloadCallback) { + var self = this; + return new Promise(function (resolve) { + function resolveWhenEditMenuDialogIsCancelled(rootID) { + return self._openEditMenuDialog(rootID, beforeReloadCallback).then(resolve); + } + if ($('[data-content_menu_id]').length) { + var select = new SelectEditMenuDialog(self); + select.on('save', self, resolveWhenEditMenuDialogIsCancelled); + select.on('cancel', self, resolve); + select.open(); + } else { + resolveWhenEditMenuDialogIsCancelled(null); + } + }); + }, + /** + * + * @param {*} rootID + * @param {function|undefied} beforeReloadCallback function that returns a promise + * @returns {Promise} + */ + _openEditMenuDialog: function (rootID, beforeReloadCallback) { + var self = this; + return new Promise(function (resolve) { + var dialog = new EditMenuDialog(self, {}, rootID); + dialog.on('save', self, function () { + // Before reloading the page after menu modification, does the + // given action to do. + if (beforeReloadCallback) { + // Reload the page so that the menu modification are shown + beforeReloadCallback().then(function () { + window.location.reload(true); + }); + } else { + window.location.reload(true); + } + }); + dialog.on('cancel', self, resolve); + dialog.open(); + }); + }, + + /** + * Retrieves the value of a page option. + * + * @private + * @param {string} name + * @returns {Promise<*>} + */ + _getPageOption: function (name) { + var option = this.pageOptions[name]; + if (!option) { + return Promise.reject(); + } + return Promise.resolve(option.value); + }, + /** + * On save, simulated page options have to be server-saved. + * + * @private + * @returns {Promise} + */ + _onSave: function () { + var self = this; + var defs = _.map(this.pageOptions, function (option, optionName) { + if (option.isDirty) { + return self._togglePageOption({ + name: optionName, + value: option.value, + }, true, true); + } + }); + return Promise.all(defs); + }, + /** + * Opens the page properties dialog. + * + * @private + * @returns {Promise} + */ + _pageProperties: function () { + var mo; + this.trigger_up('main_object_request', { + callback: function (value) { + mo = value; + }, + }); + var dialog = new PagePropertiesDialog(this, mo.id, {}).open(); + return dialog.opened(); + }, + /** + * Toggles a page option. + * + * @private + * @param {Object} params + * @param {string} params.name + * @param {*} [params.value] (change value by default true -> false -> true) + * @param {boolean} [forceSave=false] + * @param {boolean} [noReload=false] + * @returns {Promise} + */ + _togglePageOption: function (params, forceSave, noReload) { + // First check it is a website page + var mo; + this.trigger_up('main_object_request', { + callback: function (value) { + mo = value; + }, + }); + if (mo.model !== 'website.page') { + return Promise.reject(); + } + + // Check if this is a valid option + var option = this.pageOptions[params.name]; + if (!option) { + return Promise.reject(); + } + + // Toggle the value + option.setValue(params.value); + + // If simulate is true, it means we want the option to be toggled but + // not saved on the server yet + if (!forceSave) { + return Promise.resolve(); + } + + // If not, write on the server page and reload the current location + var vals = {}; + vals[params.name] = option.value; + var prom = this._rpc({ + model: 'website.page', + method: 'write', + args: [[mo.id], vals], + }); + if (noReload) { + return prom; + } + return prom.then(function () { + window.location.reload(); + return new Promise(function () {}); + }); + }, +}); + +var PageManagement = Widget.extend({ + xmlDependencies: ['/website/static/src/xml/website.xml'], + events: { + 'click a.js_page_properties': '_onPagePropertiesButtonClick', + 'click a.js_clone_page': '_onClonePageButtonClick', + 'click a.js_delete_page': '_onDeletePageButtonClick', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Retrieves the page dependencies for the given object id. + * + * @private + * @param {integer} moID + * @returns {Promise<Array>} + */ + _getPageDependencies: function (moID) { + return this._rpc({ + model: 'website', + method: 'page_search_dependencies', + args: [moID], + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onPagePropertiesButtonClick: function (ev) { + var moID = $(ev.currentTarget).data('id'); + var dialog = new PagePropertiesDialog(this,moID, {'fromPageManagement': true}).open(); + return dialog; + }, + _onClonePageButtonClick: function (ev) { + var pageId = $(ev.currentTarget).data('id'); + _clonePage.call(this, pageId); + }, + _onDeletePageButtonClick: function (ev) { + var pageId = $(ev.currentTarget).data('id'); + _deletePage.call(this, pageId, true); + }, +}); + +/** + * Deletes the page after showing a dependencies warning for the given page id. + * + * @private + * @param {integer} pageId - The ID of the page to be deleted + * @param {Boolean} fromPageManagement + * Is the function called by the page manager? + * It will affect redirect after page deletion: reload or '/' + */ +// TODO: This function should be integrated in a widget in the future +function _deletePage(pageId, fromPageManagement) { + var self = this; + new Promise(function (resolve, reject) { + // Search the page dependencies + self._getPageDependencies(pageId) + .then(function (dependencies) { + // Inform the user about those dependencies and ask him confirmation + return new Promise(function (confirmResolve, confirmReject) { + Dialog.safeConfirm(self, "", { + title: _t("Delete Page"), + $content: $(qweb.render('website.delete_page', {dependencies: dependencies})), + confirm_callback: confirmResolve, + cancel_callback: resolve, + }); + }); + }).then(function () { + // Delete the page if the user confirmed + return self._rpc({ + model: 'website.page', + method: 'unlink', + args: [pageId], + }); + }).then(function () { + if (fromPageManagement) { + window.location.reload(true); + } else { + window.location.href = '/'; + } + }, reject); + }); +} +/** + * Duplicate the page after showing the wizard to enter new page name. + * + * @private + * @param {integer} pageId - The ID of the page to be duplicate + * + */ +function _clonePage(pageId) { + var self = this; + new Promise(function (resolve, reject) { + Dialog.confirm(this, undefined, { + title: _t("Duplicate Page"), + $content: $(qweb.render('website.duplicate_page_action_dialog')), + confirm_callback: function () { + var new_page_name = this.$('#page_name').val(); + return self._rpc({ + model: 'website.page', + method: 'clone_page', + args: [pageId, new_page_name], + }).then(function (path) { + window.location.href = path; + }).guardedCatch(reject); + }, + cancel_callback: reject, + }).on('closed', null, reject); + }); +} + +websiteNavbarData.websiteNavbarRegistry.add(ContentMenu, '#content-menu'); +websiteRootData.websiteRootRegistry.add(PageManagement, '#list_website_pages'); + +return { + PagePropertiesDialog: PagePropertiesDialog, + ContentMenu: ContentMenu, + EditMenuDialog: EditMenuDialog, + MenuEntryDialog: MenuEntryDialog, + SelectEditMenuDialog: SelectEditMenuDialog, +}; +}); diff --git a/addons/website/static/src/js/menu/customize.js b/addons/website/static/src/js/menu/customize.js new file mode 100644 index 00000000..4481d0f6 --- /dev/null +++ b/addons/website/static/src/js/menu/customize.js @@ -0,0 +1,219 @@ +odoo.define('website.customizeMenu', function (require) { +'use strict'; + +var core = require('web.core'); +var Widget = require('web.Widget'); +var websiteNavbarData = require('website.navbar'); +var WebsiteAceEditor = require('website.ace'); + +var qweb = core.qweb; + +var CustomizeMenu = Widget.extend({ + xmlDependencies: ['/website/static/src/xml/website.editor.xml'], + events: { + 'show.bs.dropdown': '_onDropdownShow', + 'click .dropdown-item[data-view-key]': '_onCustomizeOptionClick', + }, + + /** + * @override + */ + willStart: function () { + this.viewName = $(document.documentElement).data('view-xmlid'); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + start: function () { + if (!this.viewName) { + _.defer(this.destroy.bind(this)); + } + + if (this.$el.is('.show')) { + this._loadCustomizeOptions(); + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Enables/Disables a view customization whose id is given. + * + * @private + * @param {string} viewKey + * @returns {Promise} + * Unresolved if the customization succeeded as the page will be + * reloaded. + * Rejected otherwise. + */ + _doCustomize: function (viewKey) { + return this._rpc({ + route: '/website/toggle_switchable_view', + params: { + 'view_key': viewKey, + }, + }).then(function () { + window.location.reload(); + return new Promise(function () {}); + }); + }, + /** + * Loads the information about the views which can be enabled/disabled on + * the current page and shows them as switchable elements in the menu. + * + * @private + * @return {Promise} + */ + _loadCustomizeOptions: function () { + if (this.__customizeOptionsLoaded) { + return Promise.resolve(); + } + this.__customizeOptionsLoaded = true; + + var $menu = this.$el.children('.dropdown-menu'); + return this._rpc({ + route: '/website/get_switchable_related_views', + params: { + key: this.viewName, + }, + }).then(function (result) { + var currentGroup = ''; + if (result.length) { + $menu.append($('<div/>', { + class: 'dropdown-divider', + role: 'separator', + })); + } + _.each(result, function (item) { + if (currentGroup !== item.inherit_id[1]) { + currentGroup = item.inherit_id[1]; + $menu.append('<li class="dropdown-header">' + currentGroup + '</li>'); + } + var $a = $('<a/>', {href: '#', class: 'dropdown-item', 'data-view-key': item.key, role: 'menuitem'}) + .append(qweb.render('website.components.switch', {id: 'switch-' + item.id, label: item.name})); + $a.find('input').prop('checked', !!item.active); + $menu.append($a); + }); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a view's related switchable element is clicked -> enable / + * disable the related view. + * + * @private + * @param {Event} ev + */ + _onCustomizeOptionClick: function (ev) { + ev.preventDefault(); + var viewKey = $(ev.currentTarget).data('viewKey'); + this._doCustomize(viewKey); + }, + /** + * @private + */ + _onDropdownShow: function () { + this._loadCustomizeOptions(); + }, +}); + +var AceEditorMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { + close_all_widgets: '_hideEditor', + edit: '_enterEditMode', + ace: '_launchAce', + }), + + /** + * Launches the ace editor automatically when the corresponding hash is in + * the page URL. + * + * @override + */ + start: function () { + if (window.location.hash.substr(0, WebsiteAceEditor.prototype.hash.length) === WebsiteAceEditor.prototype.hash) { + this._launchAce(); + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * When handling the "edit" website action, the ace editor has to be closed. + * + * @private + */ + _enterEditMode: function () { + this._hideEditor(); + }, + /** + * @private + */ + _hideEditor: function () { + if (this.globalEditor) { + this.globalEditor.do_hide(); + } + }, + /** + * Launches the ace editor to be able to edit the templates and scss files + * which are used by the current page. + * + * @private + * @returns {Promise} + */ + _launchAce: function () { + var self = this; + var prom = new Promise(function (resolve, reject) { + self.trigger_up('action_demand', { + actionName: 'close_all_widgets', + onSuccess: resolve, + }); + }); + prom.then(function () { + if (self.globalEditor) { + self.globalEditor.do_show(); + return Promise.resolve(); + } else { + var currentHash = window.location.hash; + var indexOfView = currentHash.indexOf("?res="); + var initialResID = undefined; + if (indexOfView >= 0) { + initialResID = currentHash.substr(indexOfView + ("?res=".length)); + var parsedResID = parseInt(initialResID, 10); + if (parsedResID) { + initialResID = parsedResID; + } + } + + self.globalEditor = new WebsiteAceEditor(self, $(document.documentElement).data('view-xmlid'), { + initialResID: initialResID, + defaultBundlesRestriction: [ + 'web.assets_frontend', + 'web.assets_frontend_minimal', + 'web.assets_frontend_lazy', + ], + }); + return self.globalEditor.appendTo(document.body); + } + }); + + return prom; + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(CustomizeMenu, '#customize-menu'); +websiteNavbarData.websiteNavbarRegistry.add(AceEditorMenu, '#html_editor'); + +return CustomizeMenu; +}); diff --git a/addons/website/static/src/js/menu/debug_manager.js b/addons/website/static/src/js/menu/debug_manager.js new file mode 100644 index 00000000..e932daa7 --- /dev/null +++ b/addons/website/static/src/js/menu/debug_manager.js @@ -0,0 +1,21 @@ +odoo.define('website.debugManager', function (require) { +'use strict'; + +var config = require('web.config'); +var DebugManager = require('web.DebugManager'); +var websiteNavbarData = require('website.navbar'); + +var DebugManagerMenu = websiteNavbarData.WebsiteNavbar.include({ + /** + * @override + */ + start: function () { + if (config.isDebug()) { + new DebugManager(this).prependTo(this.$('.o_menu_systray')); + } + return this._super.apply(this, arguments); + }, +}); + +return DebugManagerMenu; +}); diff --git a/addons/website/static/src/js/menu/edit.js b/addons/website/static/src/js/menu/edit.js new file mode 100644 index 00000000..d448b15d --- /dev/null +++ b/addons/website/static/src/js/menu/edit.js @@ -0,0 +1,256 @@ +odoo.define('website.editMenu', function (require) { +'use strict'; + +var core = require('web.core'); +var EditorMenu = require('website.editor.menu'); +var websiteNavbarData = require('website.navbar'); + +var _t = core._t; + +/** + * Adds the behavior when clicking on the 'edit' button (+ editor interaction) + */ +var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + assetLibs: ['web_editor.compiled_assets_wysiwyg', 'website.compiled_assets_wysiwyg'], + + xmlDependencies: ['/website/static/src/xml/website.editor.xml'], + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions, { + edit: '_startEditMode', + on_save: '_onSave', + }), + custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, { + content_will_be_destroyed: '_onContentWillBeDestroyed', + content_was_recreated: '_onContentWasRecreated', + snippet_will_be_cloned: '_onSnippetWillBeCloned', + snippet_cloned: '_onSnippetCloned', + snippet_dropped: '_onSnippetDropped', + edition_will_stopped: '_onEditionWillStop', + edition_was_stopped: '_onEditionWasStopped', + }), + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + var context; + this.trigger_up('context_get', { + extra: true, + callback: function (ctx) { + context = ctx; + }, + }); + this._editorAutoStart = (context.editable && window.location.search.indexOf('enable_editor') >= 0); + var url = window.location.href.replace(/([?&])&*enable_editor[^&#]*&?/, '\$1'); + window.history.replaceState({}, null, url); + }, + /** + * Auto-starts the editor if necessary or add the welcome message otherwise. + * + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + + // If we auto start the editor, do not show a welcome message + if (this._editorAutoStart) { + return Promise.all([def, this._startEditMode()]); + } + + // Check that the page is empty + var $wrap = this._targetForEdition().filter('#wrapwrap.homepage').find('#wrap'); + + if ($wrap.length && $wrap.html().trim() === '') { + // If readonly empty page, show the welcome message + this.$welcomeMessage = $(core.qweb.render('website.homepage_editor_welcome_message')); + this.$welcomeMessage.addClass('o_homepage_editor_welcome_message'); + this.$welcomeMessage.css('min-height', $wrap.parent('main').height() - ($wrap.outerHeight(true) - $wrap.height())); + $wrap.empty().append(this.$welcomeMessage); + } + + return def; + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Creates an editor instance and appends it to the DOM. Also remove the + * welcome message if necessary. + * + * @private + * @returns {Promise} + */ + _startEditMode: async function () { + var self = this; + if (this.editModeEnable) { + return; + } + this.trigger_up('widgets_stop_request', { + $target: this._targetForEdition(), + }); + if (this.$welcomeMessage) { + this.$welcomeMessage.detach(); // detach from the readonly rendering before the clone by summernote + } + this.editModeEnable = true; + await new EditorMenu(this).prependTo(document.body); + this._addEditorMessages(); + var res = await new Promise(function (resolve, reject) { + self.trigger_up('widgets_start_request', { + editableMode: true, + onSuccess: resolve, + onFailure: reject, + }); + }); + // Trigger a mousedown on the main edition area to focus it, + // which is required for Summernote to activate. + this.$editorMessageElements.mousedown(); + return res; + }, + /** + * On save, the editor will ask to parent widgets if something needs to be + * done first. The website navbar will receive that demand and asks to its + * action-capable components to do something. For example, the content menu + * handles page-related options saving. However, some users with limited + * access rights do not have the content menu... but the website navbar + * expects that the save action is performed. So, this empty action is + * defined here so that all users have an 'on_save' related action. + * + * @private + * @todo improve the system to somehow declare required/optional actions + */ + _onSave: function () {}, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Adds automatic editor messages on drag&drop zone elements. + * + * @private + */ + _addEditorMessages: function () { + const $target = this._targetForEdition(); + const $skeleton = $target.find('.oe_structure.oe_empty, [data-oe-type="html"]'); + this.$editorMessageElements = $skeleton.not('[data-editor-message]').attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE')); + $skeleton.attr('contenteditable', function () { return !$(this).is(':empty'); }); + }, + /** + * Returns the target for edition. + * + * @private + * @returns {JQuery} + */ + _targetForEdition: function () { + return $('#wrapwrap'); // TODO should know about this element another way + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when content will be destroyed in the page. Notifies the + * WebsiteRoot that is should stop the public widgets. + * + * @private + * @param {OdooEvent} ev + */ + _onContentWillBeDestroyed: function (ev) { + this.trigger_up('widgets_stop_request', { + $target: ev.data.$target, + }); + }, + /** + * Called when content was recreated in the page. Notifies the + * WebsiteRoot that is should start the public widgets. + * + * @private + * @param {OdooEvent} ev + */ + _onContentWasRecreated: function (ev) { + this.trigger_up('widgets_start_request', { + editableMode: true, + $target: ev.data.$target, + }); + }, + /** + * Called when edition will stop. Notifies the + * WebsiteRoot that is should stop the public widgets. + * + * @private + * @param {OdooEvent} ev + */ + _onEditionWillStop: function (ev) { + this.$editorMessageElements && this.$editorMessageElements.removeAttr('data-editor-message'); + this.trigger_up('widgets_stop_request', { + $target: this._targetForEdition(), + }); + }, + /** + * Called when edition was stopped. Notifies the + * WebsiteRoot that is should start the public widgets. + * + * @private + * @param {OdooEvent} ev + */ + _onEditionWasStopped: function (ev) { + this.trigger_up('widgets_start_request', { + $target: this._targetForEdition(), + }); + this.editModeEnable = false; + }, + /** + * Called when a snippet is about to be cloned in the page. Notifies the + * WebsiteRoot that is should destroy the animations for this snippet. + * + * @private + * @param {OdooEvent} ev + */ + _onSnippetWillBeCloned: function (ev) { + this.trigger_up('widgets_stop_request', { + $target: ev.data.$target, + }); + }, + /** + * Called when a snippet is cloned in the page. Notifies the WebsiteRoot + * that is should start the public widgets for this snippet and the snippet it + * was cloned from. + * + * @private + * @param {OdooEvent} ev + */ + _onSnippetCloned: function (ev) { + this.trigger_up('widgets_start_request', { + editableMode: true, + $target: ev.data.$target, + }); + // TODO: remove in saas-12.5, undefined $origin will restart #wrapwrap + if (ev.data.$origin) { + this.trigger_up('widgets_start_request', { + editableMode: true, + $target: ev.data.$origin, + }); + } + }, + /** + * Called when a snippet is dropped in the page. Notifies the WebsiteRoot + * that is should start the public widgets for this snippet. Also add the + * editor messages. + * + * @private + * @param {OdooEvent} ev + */ + _onSnippetDropped: function (ev) { + this.trigger_up('widgets_start_request', { + editableMode: true, + $target: ev.data.$target, + }); + this._addEditorMessages(); + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(EditPageMenu, '#edit-page-menu'); +}); diff --git a/addons/website/static/src/js/menu/mobile_view.js b/addons/website/static/src/js/menu/mobile_view.js new file mode 100644 index 00000000..668962c8 --- /dev/null +++ b/addons/website/static/src/js/menu/mobile_view.js @@ -0,0 +1,68 @@ +odoo.define('website.mobile', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var websiteNavbarData = require('website.navbar'); + +var _t = core._t; + +var MobilePreviewDialog = Dialog.extend({ + /** + * Tweaks the modal so that it appears as a phone and modifies the iframe + * rendering to show more accurate mobile view. + * + * @override + */ + start: function () { + var self = this; + this.$modal.addClass('oe_mobile_preview'); + this.$modal.on('click', '.modal-header', function () { + self.$el.toggleClass('o_invert_orientation'); + }); + this.$iframe = $('<iframe/>', { + id: 'mobile-viewport', + src: $.param.querystring(window.location.href, 'mobilepreview'), + }); + this.$iframe.on('load', function (e) { + self.$iframe.contents().find('body').removeClass('o_connected_user'); + self.$iframe.contents().find('#oe_main_menu_navbar').remove(); + }); + this.$iframe.appendTo(this.$el); + + return this._super.apply(this, arguments); + }, +}); + +var MobileMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { + 'show-mobile-preview': '_onMobilePreviewClick', + }), + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the mobile action is triggered -> instantiate the mobile + * preview dialog. + * + * @private + */ + _onMobilePreviewClick: function () { + if (this.mobilePreview && !this.mobilePreview.isDestroyed()) { + return this.mobilePreview.close(); + } + this.mobilePreview = new MobilePreviewDialog(this, { + title: _t('Mobile preview') + ' <span class="fa fa-refresh"/>', + }).open(); + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(MobileMenu, '#mobile-menu'); + +return { + MobileMenu: MobileMenu, + MobilePreviewDialog: MobilePreviewDialog, +}; +}); diff --git a/addons/website/static/src/js/menu/navbar.js b/addons/website/static/src/js/menu/navbar.js new file mode 100644 index 00000000..937392f8 --- /dev/null +++ b/addons/website/static/src/js/menu/navbar.js @@ -0,0 +1,292 @@ +odoo.define('website.navbar', function (require) { +'use strict'; + +var core = require('web.core'); +var dom = require('web.dom'); +var publicWidget = require('web.public.widget'); +var concurrency = require('web.concurrency'); +var Widget = require('web.Widget'); +var websiteRootData = require('website.root'); + +var websiteNavbarRegistry = new publicWidget.RootWidgetRegistry(); + +var WebsiteNavbar = publicWidget.RootWidget.extend({ + xmlDependencies: ['/website/static/src/xml/website.xml'], + events: _.extend({}, publicWidget.RootWidget.prototype.events || {}, { + 'click [data-action]': '_onActionMenuClick', + 'mouseover > ul > li.dropdown:not(.show)': '_onMenuHovered', + 'click .o_mobile_menu_toggle': '_onMobileMenuToggleClick', + 'mouseenter #oe_applications:not(:has(.dropdown-item))': '_onOeApplicationsHovered', + 'show.bs.dropdown #oe_applications:not(:has(.dropdown-item))': '_onOeApplicationsShow', + }), + custom_events: _.extend({}, publicWidget.RootWidget.prototype.custom_events || {}, { + 'action_demand': '_onActionDemand', + 'edit_mode': '_onEditMode', + 'readonly_mode': '_onReadonlyMode', + 'ready_to_save': '_onSave', + }), + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + var self = this; + var initPromise = new Promise(function (resolve) { + self.resolveInit = resolve; + }); + this._widgetDefs = [initPromise]; + }, + /** + * @override + */ + start: function () { + var self = this; + dom.initAutoMoreMenu(this.$('ul.o_menu_sections'), { + maxWidth: function () { + // The navbar contains different elements in community and + // enterprise, so we check for both of them here only + return self.$el.width() + - (self.$('.o_menu_systray').outerWidth(true) || 0) + - (self.$('ul#oe_applications').outerWidth(true) || 0) + - (self.$('.o_menu_toggle').outerWidth(true) || 0) + - (self.$('.o_menu_brand').outerWidth(true) || 0); + }, + }); + return this._super.apply(this, arguments).then(function () { + self.resolveInit(); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _attachComponent: function () { + var def = this._super.apply(this, arguments); + this._widgetDefs.push(def); + return def; + }, + /** + * As the WebsiteNavbar instance is designed to be unique, the associated + * registry has been instantiated outside of the class and is simply + * returned here. + * + * @override + */ + _getRegistry: function () { + return websiteNavbarRegistry; + }, + /** + * Searches for the automatic widget {@see RootWidget} which can handle that + * action. + * + * @private + * @param {string} actionName + * @param {Array} params + * @returns {Promise} + */ + _handleAction: function (actionName, params, _i) { + var self = this; + return this._whenReadyForActions().then(function () { + var defs = []; + _.each(self._widgets, function (w) { + if (!w.handleAction) { + return; + } + + var def = w.handleAction(actionName, params); + if (def !== null) { + defs.push(def); + } + }); + if (!defs.length) { + // Handle the case where all action-capable components are not + // instantiated yet (rare) -> retry some times to eventually abort + if (_i > 50) { + console.warn(_.str.sprintf("Action '%s' was not able to be handled.", actionName)); + return Promise.reject(); + } + return concurrency.delay(100).then(function () { + return self._handleAction(actionName, params, (_i || 0) + 1); + }); + } + return Promise.all(defs).then(function (values) { + if (values.length === 1) { + return values[0]; + } + return values; + }); + }); + }, + /** + * @private + * @returns {Promise} + */ + async _loadAppMenus() { + if (!this._loadAppMenusProm) { + this._loadAppMenusProm = this._rpc({ + model: 'ir.ui.menu', + method: 'load_menus_root', + args: [], + }); + const result = await this._loadAppMenusProm; + const menus = core.qweb.render('website.oe_applications_menu', { + 'menu_data': result, + }); + this.$('#oe_applications .dropdown-menu').html(menus); + } + return this._loadAppMenusProm; + }, + /** + * @private + */ + _whenReadyForActions: function () { + return Promise.all(this._widgetDefs); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the backend applications menu is hovered -> fetch the + * available menus and insert it in DOM. + * + * @private + */ + _onOeApplicationsHovered: function () { + this._loadAppMenus(); + }, + /** + * Called when the backend applications menu is opening -> fetch the + * available menus and insert it in DOM. Needed on top of hovering as the + * dropdown could be opened via keyboard (or the user could just already + * be over the dropdown when the JS is fully loaded). + * + * @private + */ + _onOeApplicationsShow: function () { + this._loadAppMenus(); + }, + /** + * Called when an action menu is clicked -> searches for the automatic + * widget {@see RootWidget} which can handle that action. + * + * @private + * @param {Event} ev + */ + _onActionMenuClick: function (ev) { + const restore = dom.addButtonLoadingEffect(ev.currentTarget); + this._handleAction($(ev.currentTarget).data('action')).then(restore).guardedCatch(restore); + }, + /** + * Called when an action is asked to be executed from a child widget -> + * searches for the automatic widget {@see RootWidget} which can handle + * that action. + */ + _onActionDemand: function (ev) { + var def = this._handleAction(ev.data.actionName, ev.data.params); + if (ev.data.onSuccess) { + def.then(ev.data.onSuccess); + } + if (ev.data.onFailure) { + def.guardedCatch(ev.data.onFailure); + } + }, + /** + * Called in response to edit mode activation -> hides the navbar. + * + * @private + */ + _onEditMode: function () { + this.$el.addClass('editing_mode'); + this.do_hide(); + }, + /** + * Called when a submenu is hovered -> automatically opens it if another + * menu was already opened. + * + * @private + * @param {Event} ev + */ + _onMenuHovered: function (ev) { + var $opened = this.$('> ul > li.dropdown.show'); + if ($opened.length) { + $opened.find('.dropdown-toggle').dropdown('toggle'); + $(ev.currentTarget).find('.dropdown-toggle').dropdown('toggle'); + } + }, + /** + * Called when the mobile menu toggle button is click -> modifies the DOM + * to open the mobile menu. + * + * @private + */ + _onMobileMenuToggleClick: function () { + this.$el.parent().toggleClass('o_mobile_menu_opened'); + }, + /** + * Called in response to edit mode activation -> hides the navbar. + * + * @private + */ + _onReadonlyMode: function () { + this.$el.removeClass('editing_mode'); + this.do_show(); + }, + /** + * Called in response to edit mode saving -> checks if action-capable + * children have something to save. + * + * @private + * @param {OdooEvent} ev + */ + _onSave: function (ev) { + ev.data.defs.push(this._handleAction('on_save')); + }, +}); + +var WebsiteNavbarActionWidget = Widget.extend({ + /** + * 'Action name' -> 'Handler name' object + * + * Any [data-action="x"] element inside the website navbar will + * automatically trigger an action "x". This action can then be handled by + * any `WebsiteNavbarActionWidget` instance if the action name "x" is + * registered in this `actions` object. + */ + actions: {}, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Checks if the widget can execute an action whose name is given, with the + * given parameters. If it is the case, execute that action. + * + * @param {string} actionName + * @param {Array} params + * @returns {Promise|null} action's promise or null if no action was found + */ + handleAction: function (actionName, params) { + var action = this[this.actions[actionName]]; + if (action) { + return Promise.resolve(action.apply(this, params || [])); + } + return null; + }, +}); + +websiteRootData.websiteRootRegistry.add(WebsiteNavbar, '#oe_main_menu_navbar'); + +return { + WebsiteNavbar: WebsiteNavbar, + websiteNavbarRegistry: websiteNavbarRegistry, + WebsiteNavbarActionWidget: WebsiteNavbarActionWidget, +}; +}); diff --git a/addons/website/static/src/js/menu/new_content.js b/addons/website/static/src/js/menu/new_content.js new file mode 100644 index 00000000..8d541210 --- /dev/null +++ b/addons/website/static/src/js/menu/new_content.js @@ -0,0 +1,350 @@ +odoo.define('website.newMenu', function (require) { +'use strict'; + +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var websiteNavbarData = require('website.navbar'); +var wUtils = require('website.utils'); +var tour = require('web_tour.tour'); + +const {qweb, _t} = core; + +var enableFlag = 'enable_new_content'; + +var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + xmlDependencies: ['/website/static/src/xml/website.editor.xml'], + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { + close_all_widgets: '_handleCloseDemand', + new_page: '_createNewPage', + }), + events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, { + 'click': '_onBackgroundClick', + 'click [data-module-id]': '_onModuleIdClick', + 'keydown': '_onBackgroundKeydown', + }), + // allow text to be customized with inheritance + newContentText: { + failed: _t('Failed to install "%s"'), + installInProgress: _t("The installation of an App is already in progress."), + installNeeded: _t('Do you want to install the "%s" App?'), + installPleaseWait: _t('Installing "%s"'), + }, + + /** + * Prepare the navigation and find the modules to install. + * Move not installed module buttons after installed modules buttons, + * but keep the original index to be able to move back the pending install + * button at its final position, so the user can click at the same place. + * + * @override + */ + start: function () { + this.pendingInstall = false; + this.$newContentMenuChoices = this.$('#o_new_content_menu_choices'); + + var $modules = this.$newContentMenuChoices.find('.o_new_content_element'); + _.each($modules, function (el, index) { + var $el = $(el); + $el.data('original-index', index); + if ($el.data('module-id')) { + $el.appendTo($el.parent()); + $el.find('a i, a p').addClass('o_uninstalled_module'); + } + }); + + this.$firstLink = this.$newContentMenuChoices.find('a:eq(0)'); + this.$lastLink = this.$newContentMenuChoices.find('a:last'); + + if ($.deparam.querystring()[enableFlag] !== undefined) { + Object.keys(tour.tours).forEach( + el => { + let element = tour.tours[el]; + if (element.steps[0].trigger == '#new-content-menu > a' + && !element.steps[0].extra_trigger) { + element.steps[0].auto = true; + } + } + ); + this._showMenu(); + } + this.$loader = $(qweb.render('website.new_content_loader')); + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Asks the user information about a new page to create, then creates it and + * redirects the user to this new page. + * + * @private + * @returns {Promise} Unresolved if there is a redirection + */ + _createNewPage: function () { + return wUtils.prompt({ + id: 'editor_new_page', + window_title: _t("New Page"), + input: _t("Page Title"), + init: function () { + var $group = this.$dialog.find('div.form-group'); + $group.removeClass('mb0'); + + var $add = $('<div/>', {'class': 'form-group mb0 row'}) + .append($('<span/>', {'class': 'offset-md-3 col-md-9 text-left'}) + .append(qweb.render('website.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")}))); + $add.find('input').prop('checked', true); + $group.after($add); + } + }).then(function (result) { + var val = result.val; + var $dialog = result.dialog; + if (!val) { + return; + } + var url = '/website/add/' + encodeURIComponent(val); + const res = wUtils.sendRequest(url, { + add_menu: $dialog.find('input[type="checkbox"]').is(':checked') || '', + }); + return new Promise(function () {}); + }); + }, + /** + * @private + */ + _handleCloseDemand: function () { + this._hideMenu(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Set the focus on the first link + * + * @private + */ + _focusFirstLink: function () { + this.$firstLink.focus(); + }, + /** + * Set the focus on the last link + * + * @private + */ + _focusLastLink: function () { + this.$lastLink.focus(); + }, + /** + * Hide the menu + * + * @private + */ + _hideMenu: function () { + this.shown = false; + this.$newContentMenuChoices.addClass('o_hidden'); + $('body').removeClass('o_new_content_open'); + }, + /** + * Install a module + * + * @private + * @param {number} moduleId: the module to install + * @return {Promise} + */ + _install: function (moduleId) { + this.pendingInstall = true; + $('body').css('pointer-events', 'none'); + return this._rpc({ + model: 'ir.module.module', + method: 'button_immediate_install', + args: [[moduleId]], + }).guardedCatch(function () { + $('body').css('pointer-events', ''); + }); + }, + /** + * Show the menu + * + * @private + * @returns {Promise} + */ + _showMenu: function () { + var self = this; + return new Promise(function (resolve, reject) { + self.trigger_up('action_demand', { + actionName: 'close_all_widgets', + onSuccess: resolve, + }); + }).then(function () { + self.firstTab = true; + self.shown = true; + self.$newContentMenuChoices.removeClass('o_hidden'); + $('body').addClass('o_new_content_open'); + self.$('> a').focus(); + + wUtils.removeLoader(); + }); + }, + /** + * Called to add loader element in DOM. + * + * @param {string} moduleName + * @private + */ + _addLoader(moduleName) { + const newContentLoaderText = _.str.sprintf(_t("Building your %s"), moduleName); + this.$loader.find('#new_content_loader_text').replaceWith(newContentLoaderText); + $('body').append(this.$loader); + }, + /** + * @private + */ + _removeLoader() { + this.$loader.remove(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the menu's toggle button is clicked: + * -> Opens the menu and reset the tab navigation (if closed) + * -> Close the menu (if open) + * Called when a click outside the menu's options occurs -> Close the menu + * + * @private + * @param {Event} ev + */ + _onBackgroundClick: function (ev) { + if (this.$newContentMenuChoices.hasClass('o_hidden')) { + this._showMenu(); + } else { + this._hideMenu(); + } + }, + /** + * Called when a keydown occurs: + * ESC -> Closes the modal + * TAB -> Navigation (captured in the modal) + * + * @private + * @param {Event} ev + */ + _onBackgroundKeydown: function (ev) { + if (!this.shown) { + return; + } + switch (ev.which) { + case $.ui.keyCode.ESCAPE: + this._hideMenu(); + ev.stopPropagation(); + break; + case $.ui.keyCode.TAB: + if (ev.shiftKey) { + if (this.firstTab || document.activeElement === this.$firstLink[0]) { + this._focusLastLink(); + ev.preventDefault(); + } + } else { + if (this.firstTab || document.activeElement === this.$lastLink[0]) { + this._focusFirstLink(); + ev.preventDefault(); + } + } + this.firstTab = false; + break; + } + }, + /** + * Open the install dialog related to an element: + * - open the dialog depending on access right and another pending install + * - if ok to install, prepare the install action: + * - call the proper action on click + * - change the button text and style + * - handle the result (reload on the same page or error) + * + * @private + * @param {Event} ev + */ + _onModuleIdClick: function (ev) { + var self = this; + var $el = $(ev.currentTarget); + var $i = $el.find('a i'); + var $p = $el.find('a p'); + + var title = $p.text(); + var content = ''; + var buttons; + + var moduleId = $el.data('module-id'); + var name = $el.data('module-shortdesc'); + + ev.stopPropagation(); + ev.preventDefault(); + + if (this.pendingInstall) { + content = this.newContentText.installInProgress; + } else { + content = _.str.sprintf(this.newContentText.installNeeded, name); + buttons = [{ + text: _t("Install"), + classes: 'btn-primary', + close: true, + click: function () { + // move the element where it will be after installation + var $finalPosition = self.$newContentMenuChoices + .find('.o_new_content_element:not([data-module-id])') + .filter(function () { + return $(this).data('original-index') < $el.data('original-index'); + }).last(); + if ($finalPosition) { + $el.fadeTo(400, 0, function () { + // if once installed, button disapeear, don't need to move it. + if (!$el.hasClass('o_new_content_element_once')) { + $el.insertAfter($finalPosition); + } + // change style to use spinner + $i.removeClass() + .addClass('fa fa-spin fa-spinner fa-pulse') + .css('background-image', 'none'); + $p.removeClass('o_uninstalled_module') + .text(_.str.sprintf(self.newContentText.installPleaseWait, name)); + $el.fadeTo(1000, 1); + self._addLoader(name); + }); + } + + self._install(moduleId).then(function () { + var origin = window.location.origin; + var redirectURL = $el.find('a').data('url') || (window.location.pathname + '?' + enableFlag); + window.location.href = origin + redirectURL; + self._removeLoader(); + }, function () { + $i.removeClass() + .addClass('fa fa-exclamation-triangle'); + $p.text(_.str.sprintf(self.newContentText.failed, name)); + }); + } + }, { + text: _t("Cancel"), + close: true, + }]; + } + + new Dialog(this, { + title: title, + size: 'medium', + $content: $('<div/>', {text: content}), + buttons: buttons + }).open(); + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(NewContentMenu, '.o_new_content_menu'); + +return NewContentMenu; +}); diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js new file mode 100644 index 00000000..b724bc1a --- /dev/null +++ b/addons/website/static/src/js/menu/seo.js @@ -0,0 +1,902 @@ +odoo.define('website.seo', function (require) { +'use strict'; + +var core = require('web.core'); +var Class = require('web.Class'); +var Dialog = require('web.Dialog'); +var mixins = require('web.mixins'); +var rpc = require('web.rpc'); +var Widget = require('web.Widget'); +var weWidgets = require('wysiwyg.widgets'); +var websiteNavbarData = require('website.navbar'); + +var _t = core._t; + +// This replaces \b, because accents(e.g. à, é) are not seen as word boundaries. +// Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b +var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)'; + +var Suggestion = Widget.extend({ + template: 'website.seo_suggestion', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click .o_seo_suggestion': 'select', + }, + + init: function (parent, options) { + this.keyword = options.keyword; + this._super(parent); + }, + select: function () { + this.trigger('selected', this.keyword); + }, +}); + +var SuggestionList = Widget.extend({ + template: 'website.seo_suggestion_list', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + + init: function (parent, options) { + this.root = options.root; + this.language = options.language; + this.htmlPage = options.htmlPage; + this._super(parent); + }, + start: function () { + this.refresh(); + }, + refresh: function () { + var self = this; + self.$el.append(_t("Loading...")); + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + var language = self.language || context.lang.toLowerCase(); + this._rpc({ + route: '/website/seo_suggest', + params: { + keywords: self.root, + lang: language, + }, + }).then(function (keyword_list) { + self.addSuggestions(JSON.parse(keyword_list)); + }); + }, + addSuggestions: function (keywords) { + var self = this; + self.$el.empty(); + // TODO Improve algorithm + Ajust based on custom user keywords + var regex = new RegExp(WORD_SEPARATORS_REGEX + self.root + WORD_SEPARATORS_REGEX, 'gi'); + keywords = _.map(_.uniq(keywords), function (word) { + return word.replace(regex, '').trim(); + }); + // TODO Order properly ? + _.each(keywords, function (keyword) { + if (keyword) { + var suggestion = new Suggestion(self, { + keyword: keyword, + }); + suggestion.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + suggestion.appendTo(self.$el); + } + }); + }, +}); + +var Keyword = Widget.extend({ + template: 'website.seo_keyword', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click a[data-action=remove-keyword]': 'destroy', + }, + + init: function (parent, options) { + this.keyword = options.word; + this.language = options.language; + this.htmlPage = options.htmlPage; + this.used_h1 = this.htmlPage.isInHeading1(this.keyword); + this.used_h2 = this.htmlPage.isInHeading2(this.keyword); + this.used_content = this.htmlPage.isInBody(this.keyword); + this._super(parent); + }, + start: function () { + var self = this; + this.$('.o_seo_keyword_suggestion').empty(); + this.suggestionList = new SuggestionList(this, { + root: this.keyword, + language: this.language, + htmlPage: this.htmlPage, + }); + this.suggestionList.on('selected', this, function (word, language) { + this.trigger('selected', word, language); + }); + return this.suggestionList.appendTo(this.$('.o_seo_keyword_suggestion')).then(function() { + self.htmlPage.on('title-changed', self, self._updateTitle); + self.htmlPage.on('description-changed', self, self._updateDescription); + self._updateTitle(); + self._updateDescription(); + }); + }, + destroy: function () { + this.trigger('removed'); + this._super(); + }, + _updateTitle: function () { + var $title = this.$('.js_seo_keyword_title'); + if (this.htmlPage.isInTitle(this.keyword)) { + $title.css('visibility', 'visible'); + } else { + $title.css('visibility', 'hidden'); + } + }, + _updateDescription: function () { + var $description = this.$('.js_seo_keyword_description'); + if (this.htmlPage.isInDescription(this.keyword)) { + $description.css('visibility', 'visible'); + } else { + $description.css('visibility', 'hidden'); + } + }, +}); + +var KeywordList = Widget.extend({ + template: 'website.seo_list', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + maxKeywords: 10, + + init: function (parent, options) { + this.htmlPage = options.htmlPage; + this._super(parent); + }, + start: function () { + var self = this; + var existingKeywords = self.htmlPage.keywords(); + if (existingKeywords.length > 0) { + _.each(existingKeywords, function (word) { + self.add.call(self, word); + }); + } + }, + keywords: function () { + var result = []; + this.$('.js_seo_keyword').each(function () { + result.push($(this).data('keyword')); + }); + return result; + }, + isFull: function () { + return this.keywords().length >= this.maxKeywords; + }, + exists: function (word) { + return _.contains(this.keywords(), word); + }, + add: async function (candidate, language) { + var self = this; + // TODO Refine + var word = candidate ? candidate.replace(/[,;.:<>]+/g, ' ').replace(/ +/g, ' ').trim().toLowerCase() : ''; + if (word && !self.isFull() && !self.exists(word)) { + var keyword = new Keyword(self, { + word: word, + language: language, + htmlPage: this.htmlPage, + }); + keyword.on('removed', self, function () { + self.trigger('list-not-full'); + self.trigger('content-updated', true); + }); + keyword.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + await keyword.appendTo(self.$el); + } + if (self.isFull()) { + self.trigger('list-full'); + } + self.trigger('content-updated'); + }, +}); + +var Preview = Widget.extend({ + template: 'website.seo_preview', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + + init: function (parent, options) { + this.title = options.title; + this.url = options.url; + this.description = options.description; + if (this.description.length > 160) { + this.description = this.description.substring(0, 159) + '…'; + } + this._super(parent); + }, +}); + +var HtmlPage = Class.extend(mixins.PropertiesMixin, { + init: function () { + mixins.PropertiesMixin.init.call(this); + this.initTitle = this.title(); + this.defaultTitle = $('meta[name="default_title"]').attr('content'); + this.initDescription = this.description(); + }, + url: function () { + return window.location.origin + window.location.pathname; + }, + title: function () { + return $('title').text().trim(); + }, + changeTitle: function (title) { + // TODO create tag if missing + $('title').text(title.trim() || this.defaultTitle); + this.trigger('title-changed', title); + }, + description: function () { + return ($('meta[name=description]').attr('content') || '').trim(); + }, + changeDescription: function (description) { + // TODO create tag if missing + $('meta[name=description]').attr('content', description); + this.trigger('description-changed', description); + }, + keywords: function () { + var $keywords = $('meta[name=keywords]'); + var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(','); + return (parsed && parsed[0]) ? parsed: []; + }, + changeKeywords: function (keywords) { + // TODO create tag if missing + $('meta[name=keywords]').attr('content', keywords.join(',')); + }, + headers: function (tag) { + return $('#wrap '+tag).map(function () { + return $(this).text(); + }); + }, + getOgMeta: function () { + var ogImageUrl = $('meta[property="og:image"]').attr('content'); + var title = $('meta[property="og:title"]').attr('content'); + var description = $('meta[property="og:description"]').attr('content'); + return { + ogImageUrl: ogImageUrl && ogImageUrl.replace(window.location.origin, ''), + metaTitle: title, + metaDescription: description, + }; + }, + images: function () { + return $('#wrap img').filter(function () { + return this.naturalHeight >= 200 && this.naturalWidth >= 200; + }).map(function () { + return { + src: this.getAttribute('src'), + alt: this.getAttribute('alt'), + }; + }); + }, + company: function () { + return $('html').attr('data-oe-company-name'); + }, + bodyText: function () { + return $('body').children().not('.oe_seo_configuration').text(); + }, + heading1: function () { + return $('body').children().not('.oe_seo_configuration').find('h1').text(); + }, + heading2: function () { + return $('body').children().not('.oe_seo_configuration').find('h2').text(); + }, + isInBody: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.bodyText()); + }, + isInTitle: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.title()); + }, + isInDescription: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.description()); + }, + isInHeading1: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.heading1()); + }, + isInHeading2: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.heading2()); + }, +}); + +var MetaTitleDescription = Widget.extend({ + // Form and preview for SEO meta title and meta description + // + // We only want to show an alert for "description too small" on those cases + // - at init and the description is not empty + // - we reached past the minimum and went back to it + // - focus out of the field + // Basically we don't want the too small alert when the field is empty and + // we start typing on it. + template: 'website.seo_meta_title_description', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'input input[name=website_meta_title]': '_titleChanged', + 'input input[name=website_seo_name]': '_seoNameChanged', + 'input textarea[name=website_meta_description]': '_descriptionOnInput', + 'change textarea[name=website_meta_description]': '_descriptionOnChange', + }, + maxRecommendedDescriptionSize: 300, + minRecommendedDescriptionSize: 50, + showDescriptionTooSmall: false, + + /** + * @override + */ + init: function (parent, options) { + this.htmlPage = options.htmlPage; + this.canEditTitle = !!options.canEditTitle; + this.canEditDescription = !!options.canEditDescription; + this.canEditUrl = !!options.canEditUrl; + this.isIndexed = !!options.isIndexed; + this.seoName = options.seoName; + this.seoNameDefault = options.seoNameDefault; + this.seoNameHelp = options.seoNameHelp; + this.previewDescription = options.previewDescription; + this._super(parent, options); + }, + /** + * @override + */ + start: function () { + this.$title = this.$('input[name=website_meta_title]'); + this.$seoName = this.$('input[name=website_seo_name]'); + this.$seoNamePre = this.$('span.seo_name_pre'); + this.$seoNamePost = this.$('span.seo_name_post'); + this.$description = this.$('textarea[name=website_meta_description]'); + this.$warning = this.$('div#website_meta_description_warning'); + this.$preview = this.$('.js_seo_preview'); + + if (!this.canEditTitle) { + this.$title.attr('disabled', true); + } + if (!this.canEditDescription) { + this.$description.attr('disabled', true); + } + if (this.htmlPage.title().trim() !== this.htmlPage.defaultTitle.trim()) { + this.$title.val(this.htmlPage.title()); + } + if (this.htmlPage.description().trim() !== this.previewDescription) { + this.$description.val(this.htmlPage.description()); + } + + if (this.canEditUrl) { + this.previousSeoName = this.seoName; + this.$seoName.val(this.seoName); + this.$seoName.attr('placeholder', this.seoNameDefault); + // make slug editable with input group for static text + const splitsUrl = window.location.pathname.split(this.previousSeoName || this.seoNameDefault); + this.$seoNamePre.text(splitsUrl[0]); + this.$seoNamePost.text(splitsUrl.slice(-1)[0]); // at least the -id theorically + } + this._descriptionOnChange(); + }, + /** + * Get the current title + */ + getTitle: function () { + return this.$title.val().trim() || this.htmlPage.defaultTitle; + }, + /** + * Get the potential new url with custom seoName as slug. + I can differ after save if slug JS != slug Python, but it provide an idea for the preview + */ + getUrl: function () { + const path = window.location.pathname.replace( + this.previousSeoName || this.seoNameDefault, + (this.$seoName.length && this.$seoName.val() ? this.$seoName.val().trim() : this.$seoName.attr('placeholder')) + ); + return window.location.origin + path + }, + /** + * Get the current description + */ + getDescription: function () { + return this.getRealDescription() || this.previewDescription; + }, + /** + * Get the current description chosen by the user + */ + getRealDescription: function () { + return this.$description.val() || ''; + }, + /** + * @private + */ + _titleChanged: function () { + var self = this; + self._renderPreview(); + self.trigger('title-changed'); + }, + /** + * @private + */ + _seoNameChanged: function () { + var self = this; + // don't use _, because we need to keep trailing whitespace during edition + const slugified = this.$seoName.val().toString().toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '-') // Remove all non-word chars + .replace(/\-\-+/g, '-'); // Replace multiple - with single - + this.$seoName.val(slugified); + self._renderPreview(); + }, + /** + * @private + */ + _descriptionOnChange: function () { + this.showDescriptionTooSmall = true; + this._descriptionOnInput(); + }, + /** + * @private + */ + _descriptionOnInput: function () { + var length = this.getDescription().length; + + if (length >= this.minRecommendedDescriptionSize) { + this.showDescriptionTooSmall = true; + } else if (length === 0) { + this.showDescriptionTooSmall = false; + } + + if (length > this.maxRecommendedDescriptionSize) { + this.$warning.text(_t('Your description looks too long.')).show(); + } else if (this.showDescriptionTooSmall && length < this.minRecommendedDescriptionSize) { + this.$warning.text(_t('Your description looks too short.')).show(); + } else { + this.$warning.hide(); + } + + this._renderPreview(); + this.trigger('description-changed'); + }, + /** + * @private + */ + _renderPreview: function () { + var indexed = this.isIndexed; + var preview = ""; + if (indexed) { + preview = new Preview(this, { + title: this.getTitle(), + description: this.getDescription(), + url: this.getUrl(), + }); + } else { + preview = new Preview(this, { + description: _t("You have hidden this page from search results. It won't be indexed by search engines."), + }); + } + this.$preview.empty(); + preview.appendTo(this.$preview); + }, +}); + +var MetaKeywords = Widget.extend({ + // Form and table for SEO meta keywords + template: 'website.seo_meta_keywords', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'keyup input[name=website_meta_keywords]': '_confirmKeyword', + 'click button[data-action=add]': '_addKeyword', + }, + + init: function (parent, options) { + this.htmlPage = options.htmlPage; + this._super(parent, options); + }, + start: function () { + var self = this; + this.$input = this.$('input[name=website_meta_keywords]'); + this.keywordList = new KeywordList(this, {htmlPage: this.htmlPage}); + this.keywordList.on('list-full', this, function () { + self.$input.attr({ + readonly: 'readonly', + placeholder: "Remove a keyword first" + }); + self.$('button[data-action=add]').prop('disabled', true).addClass('disabled'); + }); + this.keywordList.on('list-not-full', this, function () { + self.$input.removeAttr('readonly').attr('placeholder', ""); + self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled'); + }); + this.keywordList.on('selected', this, function (word, language) { + self.keywordList.add(word, language); + }); + this.keywordList.on('content-updated', this, function (removed) { + self._updateTable(removed); + }); + return this.keywordList.insertAfter(this.$('.table thead')).then(function() { + self._getLanguages(); + self._updateTable(); + }); + }, + _addKeyword: function () { + var $language = this.$('select[name=seo_page_language]'); + var keyword = this.$input.val(); + var language = $language.val().toLowerCase(); + this.keywordList.add(keyword, language); + this.$input.val('').focus(); + }, + _confirmKeyword: function (e) { + if (e.keyCode === 13) { + this._addKeyword(); + } + }, + _getLanguages: function () { + var self = this; + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + this._rpc({ + route: '/website/get_languages', + }).then(function (data) { + self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { + 'language': data, + 'def_lang': context.lang + })); + }); + }, + /* + * Show the table if there is at least one keyword. Hide it otherwise. + * + * @private + * @param {boolean} removed: a keyword is about to be removed, + * we need to exclude it from the count + */ + _updateTable: function (removed) { + var min = removed ? 1 : 0; + if (this.keywordList.keywords().length > min) { + this.$('table').show(); + } else { + this.$('table').hide(); + } + }, +}); + +var MetaImageSelector = Widget.extend({ + template: 'website.seo_meta_image_selector', + xmlDependencies: ['/website/static/src/xml/website.seo.xml'], + events: { + 'click .o_meta_img_upload': '_onClickUploadImg', + 'click .o_meta_img': '_onClickSelectImg', + }, + /** + * @override + * @param {widget} parent + * @param {Object} data + */ + init: function (parent, data) { + this.metaTitle = data.title || ''; + this.activeMetaImg = data.metaImg; + this.serverUrl = data.htmlpage.url(); + const imgField = data.hasSocialDefaultImage ? 'social_default_image' : 'logo'; + data.pageImages.unshift(_.str.sprintf('/web/image/website/%s/%s', odoo.session_info.website_id, imgField)); + this.images = _.uniq(data.pageImages); + this.customImgUrl = _.contains( + data.pageImages.map((img)=> new URL(img, window.location.origin).pathname), + new URL(data.metaImg, window.location.origin).pathname) + ? false : data.metaImg; + this.previewDescription = data.previewDescription; + this._setDescription(this.previewDescription); + this._super(parent); + }, + setTitle: function (title) { + this.metaTitle = title; + this._updateTemplateBody(); + }, + setDescription: function (description) { + this._setDescription(description); + this._updateTemplateBody(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Set the description, applying ellipsis if too long. + * + * @private + */ + _setDescription: function (description) { + this.metaDescription = description || this.previewDescription; + if (this.metaDescription.length > 160) { + this.metaDescription = this.metaDescription.substring(0, 159) + '…'; + } + }, + + /** + * Update template. + * + * @private + */ + _updateTemplateBody: function () { + this.$el.empty(); + this.images = _.uniq(this.images); + this.$el.append(core.qweb.render('website.og_image_body', {widget: this})); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when a select image from list -> change the preview accordingly. + * + * @private + * @param {MouseEvent} ev + */ + _onClickSelectImg: function (ev) { + var $img = $(ev.currentTarget); + this.activeMetaImg = $img.find('img').attr('src'); + this._updateTemplateBody(); + }, + /** + * Open a mediaDialog to select/upload image. + * + * @private + * @param {MouseEvent} ev + */ + _onClickUploadImg: function (ev) { + var self = this; + var $image = $('<img/>'); + var mediaDialog = new weWidgets.MediaDialog(this, { + onlyImages: true, + res_model: 'ir.ui.view', + }, $image[0]); + mediaDialog.open(); + mediaDialog.on('save', this, function (image) { + self.activeMetaImg = image.src; + self.customImgUrl = image.src; + self._updateTemplateBody(); + }); + }, +}); + +var SeoConfigurator = Dialog.extend({ + template: 'website.seo_configuration', + xmlDependencies: Dialog.prototype.xmlDependencies.concat( + ['/website/static/src/xml/website.seo.xml'] + ), + canEditTitle: false, + canEditDescription: false, + canEditKeywords: false, + canEditLanguage: false, + canEditUrl: false, + + init: function (parent, options) { + options = options || {}; + _.defaults(options, { + title: _t('Optimize SEO'), + buttons: [ + {text: _t('Save'), classes: 'btn-primary', click: this.update}, + {text: _t('Discard'), close: true}, + ], + }); + + this._super(parent, options); + }, + start: function () { + var self = this; + + this.$modal.addClass('oe_seo_configuration'); + + this.htmlPage = new HtmlPage(); + + this.disableUnsavableFields().then(function () { + // Image selector + self.metaImageSelector = new MetaImageSelector(self, { + htmlpage: self.htmlPage, + hasSocialDefaultImage: self.hasSocialDefaultImage, + title: self.htmlPage.getOgMeta().metaTitle, + metaImg: self.metaImg || self.htmlPage.getOgMeta().ogImageUrl, + pageImages: _.pluck(self.htmlPage.images().get(), 'src'), + previewDescription: _t('The description will be generated by social media based on page content unless you specify one.'), + }); + self.metaImageSelector.appendTo(self.$('.js_seo_image')); + + // title and description + self.metaTitleDescription = new MetaTitleDescription(self, { + htmlPage: self.htmlPage, + canEditTitle: self.canEditTitle, + canEditDescription: self.canEditDescription, + canEditUrl: self.canEditUrl, + isIndexed: self.isIndexed, + previewDescription: _t('The description will be generated by search engines based on page content unless you specify one.'), + seoNameHelp: _t('This value will be escaped to be compliant with all major browsers and used in url. Keep it empty to use the default name of the record.'), + seoName: self.seoName, // 'my-custom-display-name' or '' + seoNameDefault: self.seoNameDefault, // 'display-name' + }); + self.metaTitleDescription.on('title-changed', self, self.titleChanged); + self.metaTitleDescription.on('description-changed', self, self.descriptionChanged); + self.metaTitleDescription.appendTo(self.$('.js_seo_meta_title_description')); + + // keywords + self.metaKeywords = new MetaKeywords(self, {htmlPage: self.htmlPage}); + self.metaKeywords.appendTo(self.$('.js_seo_meta_keywords')); + }); + }, + /* + * Reset meta tags to their initial value if not saved. + * + * @private + */ + destroy: function () { + if (!this.savedData) { + this.htmlPage.changeTitle(this.htmlPage.initTitle); + this.htmlPage.changeDescription(this.htmlPage.initDescription); + } + this._super.apply(this, arguments); + }, + disableUnsavableFields: function () { + var self = this; + return this.loadMetaData().then(function (data) { + // We only need a reload for COW when the copy is happening, therefore: + // - no reload if we are not editing a view (condition: website_id === undefined) + // - reload if generic page (condition: website_id === false) + self.reloadOnSave = data.website_id === undefined ? false : !data.website_id; + //If website.page, hide the google preview & tell user his page is currently unindexed + self.isIndexed = (data && ('website_indexed' in data)) ? data.website_indexed : true; + self.canEditTitle = data && ('website_meta_title' in data); + self.canEditDescription = data && ('website_meta_description' in data); + self.canEditKeywords = data && ('website_meta_keywords' in data); + self.metaImg = data.website_meta_og_img; + self.hasSocialDefaultImage = data.has_social_default_image; + self.canEditUrl = data && ('seo_name' in data); + self.seoName = self.canEditUrl && data.seo_name; + self.seoNameDefault = self.canEditUrl && data.seo_name_default; + if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) { + // disable the button to prevent an error if the current page doesn't use the mixin + // we make the check here instead of on the view because we don't need to check + // at every page load, just when the rare case someone clicks on this link + // TODO don't show the modal but just an alert in this case + self.$footer.find('button[data-action=update]').attr('disabled', true); + } + }); + }, + update: function () { + var self = this; + var data = {}; + if (this.canEditTitle) { + data.website_meta_title = this.metaTitleDescription.$title.val(); + } + if (this.canEditDescription) { + data.website_meta_description = this.metaTitleDescription.$description.val(); + } + if (this.canEditKeywords) { + data.website_meta_keywords = this.metaKeywords.keywordList.keywords().join(', '); + } + if (this.canEditUrl) { + if (this.metaTitleDescription.$seoName.val() != this.metaTitleDescription.previousSeoName) { + data.seo_name = this.metaTitleDescription.$seoName.val(); + self.reloadOnSave = true; // will force a refresh on old url and redirect to new slug + } + } + data.website_meta_og_img = this.metaImageSelector.activeMetaImg; + this.saveMetaData(data).then(function () { + // We want to reload if we are editing a generic page + // because it will become a specific page after this change (COW) + // and we want the user to be on the page he just created. + if (self.reloadOnSave) { + window.location.href = self.htmlPage.url(); + } else { + self.htmlPage.changeKeywords(self.metaKeywords.keywordList.keywords()); + self.savedData = true; + self.close(); + } + }); + }, + getMainObject: function () { + var mainObject; + this.trigger_up('main_object_request', { + callback: function (value) { + mainObject = value; + }, + }); + return mainObject; + }, + getSeoObject: function () { + var seoObject; + this.trigger_up('seo_object_request', { + callback: function (value) { + seoObject = value; + }, + }); + return seoObject; + }, + loadMetaData: function () { + var obj = this.getSeoObject() || this.getMainObject(); + return new Promise(function (resolve, reject) { + if (!obj) { + // return Promise.reject(new Error("No main_object was found.")); + resolve(null); + } else { + rpc.query({ + route: "/website/get_seo_data", + params: { + 'res_id': obj.id, + 'res_model': obj.model, + }, + }).then(function (data) { + var meta = data; + meta.model = obj.model; + resolve(meta); + }).guardedCatch(reject); + } + }); + }, + saveMetaData: function (data) { + var obj = this.getSeoObject() || this.getMainObject(); + if (!obj) { + return Promise.reject(); + } else { + return this._rpc({ + model: obj.model, + method: 'write', + args: [[obj.id], data], + }); + } + }, + titleChanged: function () { + var self = this; + _.defer(function () { + var title = self.metaTitleDescription.getTitle(); + self.htmlPage.changeTitle(title); + self.metaImageSelector.setTitle(title); + }); + }, + descriptionChanged: function () { + var self = this; + _.defer(function () { + var description = self.metaTitleDescription.getRealDescription(); + self.htmlPage.changeDescription(description); + self.metaImageSelector.setDescription(description); + }); + }, +}); + +var SeoMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, { + 'promote-current-page': '_promoteCurrentPage', + }), + + init: function (parent, options) { + this._super(parent, options); + + if ($.deparam.querystring().enable_seo !== undefined) { + this._promoteCurrentPage(); + } + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Opens the SEO configurator dialog. + * + * @private + */ + _promoteCurrentPage: function () { + new SeoConfigurator(this).open(); + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(SeoMenu, '#promote-menu'); + +return { + SeoConfigurator: SeoConfigurator, + SeoMenu: SeoMenu, +}; +}); diff --git a/addons/website/static/src/js/menu/translate.js b/addons/website/static/src/js/menu/translate.js new file mode 100644 index 00000000..afb2aff2 --- /dev/null +++ b/addons/website/static/src/js/menu/translate.js @@ -0,0 +1,88 @@ +odoo.define('website.translateMenu', function (require) { +'use strict'; + +var utils = require('web.utils'); +var TranslatorMenu = require('website.editor.menu.translate'); +var websiteNavbarData = require('website.navbar'); + +var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({ + assetLibs: ['web_editor.compiled_assets_wysiwyg', 'website.compiled_assets_wysiwyg'], + + actions: _.extend({}, websiteNavbarData.WebsiteNavbar.prototype.actions || {}, { + edit_master: '_goToMasterPage', + translate: '_startTranslateMode', + }), + + /** + * @override + */ + start: function () { + var context; + this.trigger_up('context_get', { + extra: true, + callback: function (ctx) { + context = ctx; + }, + }); + this._mustEditTranslations = context.edit_translations; + if (this._mustEditTranslations) { + var url = window.location.href.replace(/([?&])&*edit_translations[^&#]*&?/, '\$1'); + window.history.replaceState({}, null, url); + + this._startTranslateMode(); + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Actions + //-------------------------------------------------------------------------- + + /** + * Redirects the user to the same page but in the original language and in + * edit mode. + * + * @private + * @returns {Promise} + */ + _goToMasterPage: function () { + var current = document.createElement('a'); + current.href = window.location.toString(); + current.search += (current.search ? '&' : '?') + 'enable_editor=1'; + // we are in translate mode, the pathname starts with '/<url_code/' + current.pathname = current.pathname.substr(Math.max(0, current.pathname.indexOf('/', 1))); + + var link = document.createElement('a'); + link.href = '/website/lang/default'; + link.search += (link.search ? '&' : '?') + 'r=' + encodeURIComponent(current.pathname + current.search + current.hash); + + window.location = link.href; + return new Promise(function () {}); + }, + /** + * Redirects the user to the same page in translation mode (or start the + * translator is translation mode is already enabled). + * + * @private + * @returns {Promise} + */ + _startTranslateMode: function () { + if (!this._mustEditTranslations) { + window.location.search += '&edit_translations'; + return new Promise(function () {}); + } + + var translator = new TranslatorMenu(this); + + // We don't want the BS dropdown to close + // when clicking in a element to translate + $('.dropdown-menu').on('click', '.o_editable', function (ev) { + ev.stopPropagation(); + }); + + return translator.prependTo(document.body); + }, +}); + +websiteNavbarData.websiteNavbarRegistry.add(TranslatePageMenu, '.o_menu_systray:has([data-action="translate"])'); +}); diff --git a/addons/website/static/src/js/post_link.js b/addons/website/static/src/js/post_link.js new file mode 100644 index 00000000..222216e5 --- /dev/null +++ b/addons/website/static/src/js/post_link.js @@ -0,0 +1,25 @@ +odoo.define('website.post_link', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); +const wUtils = require('website.utils'); + +publicWidget.registry.postLink = publicWidget.Widget.extend({ + selector: '.post_link', + events: { + 'click': '_onClickPost', + }, + _onClickPost: function (ev) { + ev.preventDefault(); + const url = this.el.dataset.post || this.el.href; + let data = {}; + for (let [key, value] of Object.entries(this.el.dataset)) { + if (key.startsWith('post_')) { + data[key.slice(5)] = value; + } + }; + wUtils.sendRequest(url, data); + }, +}); + +}); diff --git a/addons/website/static/src/js/set_view_track.js b/addons/website/static/src/js/set_view_track.js new file mode 100644 index 00000000..d94fcddf --- /dev/null +++ b/addons/website/static/src/js/set_view_track.js @@ -0,0 +1,89 @@ +odoo.define('website.set_view_track', function (require) { +"use strict"; + +var CustomizeMenu = require('website.customizeMenu'); +var Widget = require('web.Widget'); + +var TrackPage = Widget.extend({ + template: 'website.track_page', + xmlDependencies: ['/website/static/src/xml/track_page.xml'], + events: { + 'change #switch-track-page': '_onTrackChange', + }, + + /** + * @override + */ + start: function () { + this.$input = this.$('#switch-track-page'); + this._isTracked().then((data) => { + if (data[0]['track']) { + this.track = true; + this.$input.attr('checked', 'checked'); + } else { + this.track = false; + } + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _isTracked: function (val) { + var viewid = $('html').data('viewid'); + if (!viewid) { + return Promise.reject(); + } else { + return this._rpc({ + model: 'ir.ui.view', + method: 'read', + args: [[viewid], ['track']], + }); + } + }, + /** + * @private + */ + _onTrackChange: function (ev) { + var checkboxValue = this.$input.is(':checked'); + if (checkboxValue !== this.track) { + this.track = checkboxValue; + this._trackPage(checkboxValue); + } + }, + /** + * @private + */ + _trackPage: function (val) { + var viewid = $('html').data('viewid'); + if (!viewid) { + return Promise.reject(); + } else { + return this._rpc({ + model: 'ir.ui.view', + method: 'write', + args: [[viewid], {track: val}], + }); + } + }, +}); + +CustomizeMenu.include({ + _loadCustomizeOptions: function () { + var self = this; + var def = this._super.apply(this, arguments); + return def.then(function () { + if (!self.__trackpageLoaded) { + self.__trackpageLoaded = true; + self.trackPage = new TrackPage(self); + self.trackPage.appendTo(self.$el.children('.dropdown-menu')); + } + }); + }, +}); + +}); diff --git a/addons/website/static/src/js/show_password.js b/addons/website/static/src/js/show_password.js new file mode 100644 index 00000000..a27d1812 --- /dev/null +++ b/addons/website/static/src/js/show_password.js @@ -0,0 +1,48 @@ +// +// This file is meant to allow to switch the type of an input #password +// from password to text on mousedown on an input group. +// On mouse down, we see the password in clear text +// On mouse up, we hide it again. +// +odoo.define('website.show_password', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); + +publicWidget.registry.ShowPassword = publicWidget.Widget.extend({ + selector: '#showPass', + events: { + 'mousedown': '_onShowText', + 'touchstart': '_onShowText', + }, + + /** + * @override + */ + destroy: function () { + this._super(...arguments); + $('body').off(".ShowPassword"); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onShowPassword: function () { + this.$el.closest('.input-group').find('#password').attr('type', 'password'); + }, + /** + * @private + */ + _onShowText: function () { + $('body').one('mouseup.ShowPassword touchend.ShowPassword', this._onShowPassword.bind(this)); + this.$el.closest('.input-group').find('#password').attr('type', 'text'); + }, +}); + +return publicWidget.registry.ShowPassword; + +}); diff --git a/addons/website/static/src/js/theme_preview_form.js b/addons/website/static/src/js/theme_preview_form.js new file mode 100644 index 00000000..aada899b --- /dev/null +++ b/addons/website/static/src/js/theme_preview_form.js @@ -0,0 +1,154 @@ +odoo.define('website.theme_preview_form', function (require) { +"use strict"; + +var FormController = require('web.FormController'); +var FormView = require('web.FormView'); +var viewRegistry = require('web.view_registry'); +var core = require('web.core'); +var qweb = core.qweb; + +/* +* Common code for theme installation/update handler. +*/ +const ThemePreviewControllerCommon = { + /** + * Called to add loading effect and install/pdate the selected theme depending on action. + * + * @private + * @param {number} res_id + * @param {String} action + */ + _handleThemeAction(res_id, action) { + this.$loader = $(qweb.render('website.ThemePreview.Loader', { + 'showTips': action !== 'button_refresh_theme', + })); + let actionCallback = undefined; + this._addLoader(); + switch (action) { + case 'button_choose_theme': + actionCallback = result => this.do_action(result); + break; + case 'button_refresh_theme': + actionCallback = () => this._removeLoader(); + break; + } + const rpcData = { + model: 'ir.module.module', + method: action, + args: [res_id], + context: this.initialState.context, + }; + const rpcOptions = { + shadow: true, + }; + this._rpc(rpcData, rpcOptions) + .then(actionCallback) + .guardedCatch(() => this._removeLoader()); + }, + /** + * Called to add loader element in DOM. + * + * @private + */ + _addLoader() { + $('body').append(this.$loader); + }, + /** + * @private + */ + _removeLoader() { + this.$loader.remove(); + } +}; + +var ThemePreviewController = FormController.extend(ThemePreviewControllerCommon, { + events: Object.assign({}, FormController.prototype.events, { + 'click .o_use_theme': '_onStartNowClick', + 'click .o_switch_theme': '_onSwitchThemeClick', + 'change input[name="viewer"]': '_onSwitchButtonChange', + }), + /** + * @override + */ + start: function () { + this.$el.addClass('o_view_form_theme_preview_controller'); + return this._super.apply(this, arguments); + }, + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- + + /** + * @override + */ + renderButtons: function ($node) { + this.$buttons = $(qweb.render('website.ThemePreview.Buttons')); + if ($node) { + $node.html(this.$buttons); + } + }, + /** + * Overriden to prevent the controller from hiding the buttons + * @see FormController + * + * @override + */ + updateButtons: function () { }, + + // ------------------------------------------------------------------------- + // Private + // ------------------------------------------------------------------------- + /** + * Add Switcher View Mobile / Desktop near pager + * + * @private + */ + _updateControlPanelProps: async function () { + const props = this._super(...arguments); + const $switchModeButton = $(qweb.render('website.ThemePreview.SwitchModeButton')); + this.controlPanelProps.cp_content.$pager = $switchModeButton; + return props; + }, + + // ------------------------------------------------------------------------- + // Handlers + // ------------------------------------------------------------------------- + /** + * Handler called when user click on 'Desktop/Mobile' switcher button. + * + * @private + */ + _onSwitchButtonChange: function () { + this.$('.o_preview_frame').toggleClass('is_mobile'); + }, + /** + * Handler called when user click on 'Choose another theme' button. + * + * @private + */ + _onSwitchThemeClick: function () { + this.trigger_up('history_back'); + }, + /** + * Handler called when user click on 'START NOW' button in form view. + * + * @private + */ + _onStartNowClick: function () { + this._handleThemeAction(this.getSelectedIds()[0], 'button_choose_theme'); + }, +}); + +var ThemePreviewFormView = FormView.extend({ + config: _.extend({}, FormView.prototype.config, { + Controller: ThemePreviewController + }), +}); + +viewRegistry.add('theme_preview_form', ThemePreviewFormView); + +return { + ThemePreviewControllerCommon: ThemePreviewControllerCommon +} +}); diff --git a/addons/website/static/src/js/theme_preview_kanban.js b/addons/website/static/src/js/theme_preview_kanban.js new file mode 100644 index 00000000..a8c573b9 --- /dev/null +++ b/addons/website/static/src/js/theme_preview_kanban.js @@ -0,0 +1,61 @@ +odoo.define('website.theme_preview_kanban', function (require) { +"use strict"; + +var KanbanController = require('web.KanbanController'); +var KanbanView = require('web.KanbanView'); +var ViewRegistry = require('web.view_registry'); +const ThemePreviewControllerCommon = require('website.theme_preview_form').ThemePreviewControllerCommon; +var core = require('web.core'); +var _lt = core._lt; + +var ThemePreviewKanbanController = KanbanController.extend(ThemePreviewControllerCommon, { + /** + * @override + */ + start: async function () { + await this._super(...arguments); + + // hide pager + this.el.classList.add('o_view_kanban_theme_preview_controller'); + + // update breacrumb + const websiteLink = Object.assign(document.createElement('a'), { + className: 'btn btn-secondary ml-3 text-black-75', + href: '/', + innerHTML: '<i class="fa fa-close"></i>', + }); + const smallBreadcumb = Object.assign(document.createElement('small'), { + className: 'mx-2 text-muted', + innerHTML: _lt("Don't worry, you can switch later."), + }); + this._controlPanelWrapper.el.querySelector('.o_cp_top .breadcrumb li.active').classList.add('text-black-75'); + this._controlPanelWrapper.el.querySelector('.o_cp_top').appendChild(websiteLink); + this._controlPanelWrapper.el.querySelector('.o_cp_top li').appendChild(smallBreadcumb); + }, + /** + * Called when user click on any button in kanban view. + * Targeted buttons are selected using name attribute value. + * + * @override + */ + _onButtonClicked: function (ev) { + const attrName = ev.data.attrs.name; + if (attrName === 'button_choose_theme' || attrName === 'button_refresh_theme') { + this._handleThemeAction(ev.data.record.res_id, attrName); + } else { + this._super(...arguments); + } + }, +}); + +var ThemePreviewKanbanView = KanbanView.extend({ + withSearchBar: false, // hide searchBar + + config: _.extend({}, KanbanView.prototype.config, { + Controller: ThemePreviewKanbanController, + }), +}); + +ViewRegistry.add('theme_preview_kanban', ThemePreviewKanbanView); + +}); diff --git a/addons/website/static/src/js/tours/homepage.js b/addons/website/static/src/js/tours/homepage.js new file mode 100644 index 00000000..3b4c340f --- /dev/null +++ b/addons/website/static/src/js/tours/homepage.js @@ -0,0 +1,47 @@ +odoo.define("website.tour.homepage", function (require) { +"use strict"; + +const wTourUtils = require("website.tour_utils"); + +const snippets = [ + { + id: 's_cover', + name: 'Cover', + }, + { + id: 's_text_image', + name: 'Text - Image', + }, + { + id: 's_three_columns', + name: 'Columns', + }, + { + id: 's_picture', + name: 'Picture', + }, + { + id: 's_quotes_carousel', + name: 'Quotes', + }, + { + id: 's_call_to_action', + name: 'Call to Action', + }, +]; + +wTourUtils.registerThemeHomepageTour('homepage', [ + wTourUtils.dragNDrop(snippets[0]), + wTourUtils.clickOnText(snippets[0], 'h1'), + wTourUtils.goBackToBlocks(), + wTourUtils.dragNDrop(snippets[1]), + wTourUtils.dragNDrop(snippets[2]), + wTourUtils.dragNDrop(snippets[3]), + wTourUtils.dragNDrop(snippets[4]), + wTourUtils.dragNDrop(snippets[5]), + wTourUtils.clickOnSnippet(snippets[5], 'top'), + wTourUtils.changeBackgroundColor(), + wTourUtils.clickOnSave(), +]); + +}); diff --git a/addons/website/static/src/js/tours/tour_utils.js b/addons/website/static/src/js/tours/tour_utils.js new file mode 100644 index 00000000..6a0d23d9 --- /dev/null +++ b/addons/website/static/src/js/tours/tour_utils.js @@ -0,0 +1,291 @@ +odoo.define("website.tour_utils", function (require) { +"use strict"; + +const core = require("web.core"); +const _t = core._t; + +var tour = require("web_tour.tour"); + +/** + +const snippets = [ + { + id: 's_cover', + name: 'Cover', + }, + { + id: 's_text_image', + name: 'Text - Image', + } +]; + +tour.register("themename_tour", { + url: "/", + saveAs: "homepage", +}, [ + wTourUtils.dragNDrop(snippets[0]), + wTourUtils.clickOnText(snippets[0], 'h1'), + wTourUtils.changeOption('colorFilter', 'span.o_we_color_preview', _t('color filter')), + wTourUtils.selectHeader(), + wTourUtils.changeOption('HeaderTemplate', '[data-name="header_alignment_opt"]', _t('alignment')), + wTourUtils.goBackToBlocks(), + wTourUtils.dragNDrop(snippets[1]), + wTourUtils.changeImage(snippets[1]), + wTourUtils.clickOnSave(), +]); +**/ + + + +function addMedia(position = "right") { + return { + trigger: `.modal-content footer .btn-primary`, + content: _t("<b>Add</b> the selected image."), + position: position, + run: "click", + }; +} + +function changeBackground(snippet, position = "bottom") { + return { + trigger: ".o_we_customize_panel .o_we_edit_image", + content: _t("<b>Customize</b> any block through this menu. Try to change the background image of this block."), + position: position, + run: "click", + }; +} + +function changeBackgroundColor(position = "bottom") { + return { + trigger: ".o_we_customize_panel .o_we_color_preview", + content: _t("<b>Customize</b> any block through this menu. Try to change the background color of this block."), + position: position, + run: "click", + }; +} + +function selectColorPalette(position = "left") { + return { + trigger: ".o_we_customize_panel .o_we_so_color_palette we-selection-items", + alt_trigger: ".o_we_customize_panel .o_we_color_preview", + content: _t(`<b>Select</b> a Color Palette.`), + position: position, + run: 'click', + location: position === 'left' ? '#oe_snippets' : undefined, + }; +} + +function changeColumnSize(position = "right") { + return { + trigger: `.oe_overlay.ui-draggable.o_we_overlay_sticky.oe_active .o_handle.e`, + content: _t("<b>Slide</b> this button to change the column size."), + position: position, + }; +} + +function changeIcon(snippet, index = 0, position = "bottom") { + return { + trigger: `#wrapwrap .${snippet.id} i:eq(${index})`, + content: _t("<b>Double click on an icon</b> to change it with one of your choice."), + position: position, + run: "dblclick", + }; +} + +function changeImage(snippet, position = "bottom") { + return { + trigger: `#wrapwrap .${snippet.id} img`, + content: _t("<b>Double click on an image</b> to change it with one of your choice."), + position: position, + run: "dblclick", + }; +} + +/** + wTourUtils.changeOption('HeaderTemplate', '[data-name="header_alignment_opt"]', _t('alignment')), +*/ +function changeOption(optionName, weName = '', optionTooltipLabel = '', position = "bottom") { + const option_block = `we-customizeblock-option[class='snippet-option-${optionName}']` + return { + trigger: `${option_block} ${weName}, ${option_block} [title='${weName}']`, + content: _.str.sprintf(_t("<b>Click</b> on this option to change the %s of the block."), optionTooltipLabel), + position: position, + run: "click", + }; +} + +function selectNested(trigger, optionName, alt_trigger = null, optionTooltipLabel = '', position = "top") { + const option_block = `we-customizeblock-option[class='snippet-option-${optionName}']`; + return { + trigger: trigger, + content: _.str.sprintf(_t("<b>Select</b> a %s."), optionTooltipLabel), + alt_trigger: alt_trigger == null ? undefined : `${option_block} ${alt_trigger}`, + position: position, + run: 'click', + location: position === 'left' ? '#oe_snippets' : undefined, + }; +} + +function changePaddingSize(direction) { + let paddingDirection = "n"; + let position = "top"; + if (direction === "bottom") { + paddingDirection = "s"; + position = "bottom"; + } + return { + trigger: `.oe_overlay.ui-draggable.o_we_overlay_sticky.oe_active .o_handle.${paddingDirection}`, + content: _.str.sprintf(_t("<b>Slide</b> this button to change the %s padding"), direction), + position: position, + }; +} + +/** + * Click on the top right edit button + * @param {*} position Where the purple arrow will show up + */ +function clickOnEdit(position = "bottom") { + return { + trigger: "a[data-action=edit]", + content: _t("<b>Click Edit</b> to start designing your homepage."), + extra_trigger: ".homepage", + position: position, + }; +} + +/** + * Simple click on a snippet in the edition area + * @param {*} snippet + * @param {*} position + */ +function clickOnSnippet(snippet, position = "bottom") { + return { + trigger: `#wrapwrap .${snippet.id}`, + content: _t("<b>Click on a snippet</b> to access its options menu."), + position: position, + run: "click", + }; +} + +function clickOnSave(position = "bottom") { + return { + trigger: "button[data-action=save]", + in_modal: false, + content: _t("Good job! It's time to <b>Save</b> your work."), + position: position, + }; +} + +/** + * Click on a snippet's text to modify its content + * @param {*} snippet + * @param {*} element Target the element which should be rewrite + * @param {*} position + */ +function clickOnText(snippet, element, position = "bottom") { + return { + trigger: `#wrapwrap .${snippet.id} ${element}`, + content: _t("<b>Click on a text</b> to start editing it."), + position: position, + run: "text", + consumeEvent: "input", + }; +} + +/** + * Drag a snippet from the Blocks area and drop it in the Edit area + * @param {*} snippet contain the id and the name of the targeted snippet + * @param {*} position Where the purple arrow will show up + */ +function dragNDrop(snippet, position = "bottom") { + return { + trigger: `#oe_snippets .oe_snippet[name="${snippet.name}"] .oe_snippet_thumbnail:not(.o_we_already_dragging)`, + extra_trigger: "body.editor_enable.editor_has_snippets", + moveTrigger: '.oe_drop_zone', + content: _.str.sprintf(_t("Drag the <b>%s</b> building block and drop it at the bottom of the page."), snippet.name), + position: position, + run: "drag_and_drop #wrap", + }; +} + +function goBackToBlocks(position = "bottom") { + return { + trigger: '.o_we_add_snippet_btn', + content: _t("Click here to go back to block tab."), + position: position, + run: "click", + }; +} + +function goToOptions(position = "bottom") { + return { + trigger: '.o_we_customize_theme_btn', + content: _t("Go to the Options tab"), + position: position, + run: "click", + }; +} + +function selectHeader(position = "bottom") { + return { + trigger: `header#top`, + content: _t(`<b>Click</b> on this header to configure it.`), + position: position, + run: "click", + }; +} + +function selectSnippetColumn(snippet, index = 0, position = "bottom") { + return { + trigger: `#wrapwrap .${snippet.id} .row div[class*="col-lg-"]:eq(${index})`, + content: _t("<b>Click</b> on this column to access its options."), + position: position, + run: "click", + }; +} + +function prepend_trigger(steps, prepend_text='') { + for (const step of steps) { + if (!step.noPrepend && prepend_text) { + step.trigger = prepend_text + step.trigger; + } + } + return steps; +} + +function registerThemeHomepageTour(name, steps) { + tour.register(name, { + url: "/?enable_editor=1", + sequence: 1010, + saveAs: "homepage", + }, prepend_trigger( + steps, + "html[data-view-xmlid='website.homepage'] " + )); +} + + +return { + addMedia, + changeBackground, + changeBackgroundColor, + changeColumnSize, + changeIcon, + changeImage, + changeOption, + changePaddingSize, + clickOnEdit, + clickOnSave, + clickOnSnippet, + clickOnText, + dragNDrop, + goBackToBlocks, + goToOptions, + selectColorPalette, + selectHeader, + selectNested, + selectSnippetColumn, + + registerThemeHomepageTour, +}; +}); diff --git a/addons/website/static/src/js/user_custom_javascript.js b/addons/website/static/src/js/user_custom_javascript.js new file mode 100644 index 00000000..a68d298b --- /dev/null +++ b/addons/website/static/src/js/user_custom_javascript.js @@ -0,0 +1,24 @@ +// +// This file is meant to regroup your javascript code. You can either copy/past +// any code that should be executed on each page loading or write your own +// taking advantage of the Odoo framework to create new behaviors or modify +// existing ones. For example, doing this will greet any visitor with a 'Hello, +// world !' message in a popup: +// +/* +odoo.define('website.user_custom_code', function (require) { +'use strict'; + +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); + +publicWidget.registry.HelloWorldPopup = publicWidget.Widget.extend({ + selector: '#wrapwrap', + + start: function () { + Dialog.alert(this, "Hello, world!"); + return this._super.apply(this, arguments); + }, +}) +}); +*/ diff --git a/addons/website/static/src/js/utils.js b/addons/website/static/src/js/utils.js new file mode 100644 index 00000000..1c0edf43 --- /dev/null +++ b/addons/website/static/src/js/utils.js @@ -0,0 +1,295 @@ +odoo.define('website.utils', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); + +var qweb = core.qweb; + +/** + * Allows to load anchors from a page. + * + * @param {string} url + * @returns {Deferred<string[]>} + */ +function loadAnchors(url) { + return new Promise(function (resolve, reject) { + if (url === window.location.pathname || url[0] === '#') { + resolve(document.body.outerHTML); + } else if (url.length && !url.startsWith("http")) { + $.get(window.location.origin + url).then(resolve, reject); + } else { // avoid useless query + resolve(); + } + }).then(function (response) { + return _.map($(response).find('[id][data-anchor=true]'), function (el) { + return '#' + el.id; + }); + }).catch(error => { + console.debug(error); + return []; + }); +} + +/** + * Allows the given input to propose existing website URLs. + * + * @param {ServicesMixin|Widget} self - an element capable to trigger an RPC + * @param {jQuery} $input + */ +function autocompleteWithPages(self, $input, options) { + $.widget("website.urlcomplete", $.ui.autocomplete, { + options: options || {}, + _create: function () { + this._super(); + this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)"); + }, + _renderMenu: function (ul, items) { + const self = this; + items.forEach(item => { + if (item.separator) { + self._renderSeparator(ul, item); + } + else { + self._renderItem(ul, item); + } + }); + }, + _renderSeparator: function (ul, item) { + return $("<li class='ui-autocomplete-category font-weight-bold text-capitalize p-2'>") + .append(`<div>${item.separator}</div>`) + .appendTo(ul); + }, + _renderItem: function (ul, item) { + return $("<li>") + .data('ui-autocomplete-item', item) + .append(`<div>${item.label}</div>`) + .appendTo(ul); + }, + }); + $input.urlcomplete({ + source: function (request, response) { + if (request.term[0] === '#') { + loadAnchors(request.term).then(function (anchors) { + response(anchors); + }); + } else if (request.term.startsWith('http') || request.term.length === 0) { + // avoid useless call to /website/get_suggested_links + response(); + } else { + return self._rpc({ + route: '/website/get_suggested_links', + params: { + needle: request.term, + limit: 15, + } + }).then(function (res) { + let choices = res.matching_pages; + res.others.forEach(other => { + if (other.values.length) { + choices = choices.concat( + [{separator: other.title}], + other.values, + ); + } + }); + response(choices); + }); + } + }, + select: function (ev, ui) { + // choose url in dropdown with arrow change ev.target.value without trigger_up + // so cannot check here if value has been updated + ev.target.value = ui.item.value; + self.trigger_up('website_url_chosen'); + ev.preventDefault(); + }, + }); +} + +/** + * @param {jQuery} $element + * @param {jQuery} [$excluded] + */ +function onceAllImagesLoaded($element, $excluded) { + var defs = _.map($element.find('img').addBack('img'), function (img) { + if (img.complete || $excluded && ($excluded.is(img) || $excluded.has(img).length)) { + return; // Already loaded + } + var def = new Promise(function (resolve, reject) { + $(img).one('load', function () { + resolve(); + }); + }); + return def; + }); + return Promise.all(defs); +} + +/** + * @deprecated + * @todo create Dialog.prompt instead of this + */ +function prompt(options, _qweb) { + /** + * A bootstrapped version of prompt() albeit asynchronous + * This was built to quickly prompt the user with a single field. + * For anything more complex, please use editor.Dialog class + * + * Usage Ex: + * + * website.prompt("What... is your quest ?").then(function (answer) { + * arthur.reply(answer || "To seek the Holy Grail."); + * }); + * + * website.prompt({ + * select: "Please choose your destiny", + * init: function () { + * return [ [0, "Sub-Zero"], [1, "Robo-Ky"] ]; + * } + * }).then(function (answer) { + * mame_station.loadCharacter(answer); + * }); + * + * @param {Object|String} options A set of options used to configure the prompt or the text field name if string + * @param {String} [options.window_title=''] title of the prompt modal + * @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title + * @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title + * @param {String} [options.select] tell the modal to use a select box, the given value will be the field title + * @param {Object} [options.default=''] default value of the field + * @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a promise] + */ + if (typeof options === 'string') { + options = { + text: options + }; + } + var xmlDef; + if (_.isUndefined(_qweb)) { + _qweb = 'website.prompt'; + xmlDef = ajax.loadXML('/website/static/src/xml/website.xml', core.qweb); + } + options = _.extend({ + window_title: '', + field_name: '', + 'default': '', // dict notation for IE<9 + init: function () {}, + }, options || {}); + + var type = _.intersection(Object.keys(options), ['input', 'textarea', 'select']); + type = type.length ? type[0] : 'input'; + options.field_type = type; + options.field_name = options.field_name || options[type]; + + var def = new Promise(function (resolve, reject) { + Promise.resolve(xmlDef).then(function () { + var dialog = $(qweb.render(_qweb, options)).appendTo('body'); + options.$dialog = dialog; + var field = dialog.find(options.field_type).first(); + field.val(options['default']); // dict notation for IE<9 + field.fillWith = function (data) { + if (field.is('select')) { + var select = field[0]; + data.forEach(function (item) { + select.options[select.options.length] = new window.Option(item[1], item[0]); + }); + } else { + field.val(data); + } + }; + var init = options.init(field, dialog); + Promise.resolve(init).then(function (fill) { + if (fill) { + field.fillWith(fill); + } + dialog.modal('show'); + field.focus(); + dialog.on('click', '.btn-primary', function () { + var backdrop = $('.modal-backdrop'); + resolve({ val: field.val(), field: field, dialog: dialog }); + dialog.modal('hide').remove(); + backdrop.remove(); + }); + }); + dialog.on('hidden.bs.modal', function () { + var backdrop = $('.modal-backdrop'); + reject(); + dialog.remove(); + backdrop.remove(); + }); + if (field.is('input[type="text"], select')) { + field.keypress(function (e) { + if (e.which === 13) { + e.preventDefault(); + dialog.find('.btn-primary').trigger('click'); + } + }); + } + }); + }); + + return def; +} + +function websiteDomain(self) { + var websiteID; + self.trigger_up('context_get', { + callback: function (ctx) { + websiteID = ctx['website_id']; + }, + }); + return ['|', ['website_id', '=', false], ['website_id', '=', websiteID]]; +} + +function sendRequest(route, params) { + function _addInput(form, name, value) { + let param = document.createElement('input'); + param.setAttribute('type', 'hidden'); + param.setAttribute('name', name); + param.setAttribute('value', value); + form.appendChild(param); + } + + let form = document.createElement('form'); + form.setAttribute('action', route); + form.setAttribute('method', params.method || 'POST'); + + if (core.csrf_token) { + _addInput(form, 'csrf_token', core.csrf_token); + } + + for (const key in params) { + const value = params[key]; + if (Array.isArray(value) && value.length) { + for (const val of value) { + _addInput(form, key, val); + } + } else { + _addInput(form, key, value); + } + } + + document.body.appendChild(form); + form.submit(); +} + +/** + * Removes the navigation-blocking fullscreen loader from the DOM + */ +function removeLoader() { + const $loader = $('#o_website_page_loader'); + if ($loader) { + $loader.remove(); + } +} + +return { + loadAnchors: loadAnchors, + autocompleteWithPages: autocompleteWithPages, + onceAllImagesLoaded: onceAllImagesLoaded, + prompt: prompt, + sendRequest: sendRequest, + websiteDomain: websiteDomain, + removeLoader: removeLoader, +}; +}); diff --git a/addons/website/static/src/js/visitor_timezone.js b/addons/website/static/src/js/visitor_timezone.js new file mode 100644 index 00000000..b8441bea --- /dev/null +++ b/addons/website/static/src/js/visitor_timezone.js @@ -0,0 +1 @@ +// To remove after v14.0
\ No newline at end of file diff --git a/addons/website/static/src/js/widget_iframe.js b/addons/website/static/src/js/widget_iframe.js new file mode 100644 index 00000000..8829ffa8 --- /dev/null +++ b/addons/website/static/src/js/widget_iframe.js @@ -0,0 +1,28 @@ +odoo.define('website.iframe_widget', function (require) { +"use strict"; + + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); +var fieldRegistry = require('web.field_registry'); + +var QWeb = core.qweb; + +/** + * Display iframe + */ +var FieldIframePreview = AbstractField.extend({ + className: 'd-block o_field_iframe_preview m-0 h-100', + + _render: function () { + this.$el.html(QWeb.render('website.iframeWidget', { + url: this.value, + })); + }, +}); + +fieldRegistry.add('iframe', FieldIframePreview); + +return FieldIframePreview; + +}); diff --git a/addons/website/static/src/js/widgets/ace.js b/addons/website/static/src/js/widgets/ace.js new file mode 100644 index 00000000..177a4db3 --- /dev/null +++ b/addons/website/static/src/js/widgets/ace.js @@ -0,0 +1,92 @@ +odoo.define("website.ace", function (require) { +"use strict"; + +var AceEditor = require('web_editor.ace'); + +/** + * Extends the default view editor so that the URL hash is updated with view ID + */ +var WebsiteAceEditor = AceEditor.extend({ + hash: '#advanced-view-editor', + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + do_hide: function () { + this._super.apply(this, arguments); + window.location.hash = ""; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _displayResource: function () { + this._super.apply(this, arguments); + this._updateHash(); + }, + /** + * @override + */ + _saveResources: function () { + return this._super.apply(this, arguments).then((function () { + var defs = []; + if (this.currentType === 'xml') { + // When saving a view, the view ID might change. Thus, the + // active ID in the URL will be incorrect. After the save + // reload, that URL ID won't be found and JS will crash. + // We need to find the new ID (either because the view became + // specific or because its parent was edited too and the view + // got copy/unlink). + var selectedView = _.findWhere(this.views, {id: this._getSelectedResource()}); + var context; + this.trigger_up('context_get', { + callback: function (ctx) { + context = ctx; + }, + }); + defs.push(this._rpc({ + model: 'ir.ui.view', + method: 'search_read', + fields: ['id'], + domain: [['key', '=', selectedView.key], ['website_id', '=', context.website_id]], + }).then((function (view) { + if (view[0]) { + this._updateHash(view[0].id); + } + }).bind(this))); + } + return Promise.all(defs).then((function () { + window.location.reload(); + return new Promise(function () {}); + })); + }).bind(this)); + }, + /** + * @override + */ + _resetResource: function () { + return this._super.apply(this, arguments).then((function () { + window.location.reload(); + return new Promise(function () {}); + }).bind(this)); + }, + /** + * Adds the current resource ID in the URL. + * + * @private + */ + _updateHash: function (resID) { + window.location.hash = this.hash + "?res=" + (resID || this._getSelectedResource()); + }, +}); + +return WebsiteAceEditor; +}); diff --git a/addons/website/static/src/js/widgets/media.js b/addons/website/static/src/js/widgets/media.js new file mode 100644 index 00000000..900f5306 --- /dev/null +++ b/addons/website/static/src/js/widgets/media.js @@ -0,0 +1,14 @@ +odoo.define('website.widgets.media', function (require) { +'use strict'; + +const {ImageWidget} = require('wysiwyg.widgets.media'); + +ImageWidget.include({ + _getAttachmentsDomain() { + const domain = this._super(...arguments); + domain.push('|', ['url', '=', false], '!', ['url', '=like', '/web/image/website.%']); + domain.push(['key', '=', false]); + return domain; + } +}); +}); diff --git a/addons/website/static/src/scss/bootstrap_overridden.scss b/addons/website/static/src/scss/bootstrap_overridden.scss new file mode 100644 index 00000000..e43f0684 --- /dev/null +++ b/addons/website/static/src/scss/bootstrap_overridden.scss @@ -0,0 +1,132 @@ +// +// Color system +// + +// Use auto threshold for yiq colors +// Note: also need to be defined here so that color-yiq below works +$yiq-contrasted-threshold: false !default; + +// Customize the light and dark text colors for use in our YIQ color contrast function. +$yiq-text-dark: o-color('900') !default; +$yiq-text-light: o-color('white') !default; + +// Spacing +// +// Control the default styling of most Bootstrap elements by modifying these +// variables. Mostly focused on spacing. +// You can add more entries to the $spacers map, should you need more variation. + +$spacer: 1rem !default; // Need to predefine as used below + +// Body +// +// Settings for the `<body>` element. + +$body-bg: if(o-website-value('layout') != 'full', o-color('body'), o-color('o-cc1-bg')) !default; +$body-color: o-color('o-cc1-text') or color-yiq(o-color('o-cc1-bg')) !default; + +// Links +// +// Style anchor elements. + +$-link-color: o-color('o-cc1-link'); +$-link-color: if($-link-color, $-link-color, o-color('primary')); +$link-color: auto-contrast($-link-color, o-color('o-cc1-bg'), 'o-cc1-link') !default; +$link-hover-color: auto-contrast(darken($link-color, 15%), o-color('o-cc1-bg'), 'o-cc1-link') !default; +$link-decoration: if(o-website-value('link-underline') == 'always', underline, none) !default; +$link-hover-decoration: if(o-website-value('link-underline') != 'never', underline, none) !default; + +// Components +// +// Define common padding and border radius sizes and more. + +// Note: for the 'active' color, color preset edition is not really flexible but +// this could come in a future update. +$component-active-bg: o-color('o-cc1-btn-primary') !default; +$component-active-color: if($component-active-bg, color-yiq($component-active-bg), null) !default; + +// Fonts +// +// Font, line-height, and color for body text, headings, and more. + +$font-family-sans-serif: $o-theme-font !default; + +$font-size-base: o-website-value('font-size-base') !default; + +$h1-font-size: $font-size-base * $o-theme-h1-font-size-multiplier !default; +$h2-font-size: $font-size-base * $o-theme-h2-font-size-multiplier !default; +$h3-font-size: $font-size-base * $o-theme-h3-font-size-multiplier !default; +$h4-font-size: $font-size-base * $o-theme-h4-font-size-multiplier !default; +$h5-font-size: $font-size-base * $o-theme-h5-font-size-multiplier !default; +$h6-font-size: $font-size-base * $o-theme-h6-font-size-multiplier !default; + +$headings-font-family: $o-theme-headings-font !default; +$headings-color: o-color('o-cc1-headings') !default; + +$text-muted: mute-color($body-color) !default; + +// Buttons +// +// For each of Bootstrap's buttons, define text, background, and border color. + +$btn-padding-y: o-website-value('btn-padding-y') !default; +$btn-padding-x: o-website-value('btn-padding-x') !default; +$btn-font-size: o-website-value('btn-font-size') !default; + +$btn-padding-y-sm: o-website-value('btn-padding-y-sm') !default; +$btn-padding-x-sm: o-website-value('btn-padding-x-sm') !default; +$btn-font-size-sm: o-website-value('btn-font-size-sm') !default; + +$btn-padding-y-lg: o-website-value('btn-padding-y-lg') !default; +$btn-padding-x-lg: o-website-value('btn-padding-x-lg') !default; +$btn-font-size-lg: o-website-value('btn-font-size-lg') !default; + +$btn-border-width: o-website-value('btn-border-width') !default; + +$btn-border-radius: o-website-value('btn-border-radius') !default; +$btn-border-radius-lg: o-website-value('btn-border-radius-lg') !default; +$btn-border-radius-sm: o-website-value('btn-border-radius-sm') !default; + +// Forms + +$input-padding-y: o-website-value('input-padding-y') !default; +$input-padding-x: o-website-value('input-padding-x') !default; +$input-font-size: o-website-value('input-font-size') !default; + +$input-padding-y-sm: o-website-value('input-padding-y-sm') !default; +$input-padding-x-sm: o-website-value('input-padding-x-sm') !default; +$input-font-size-sm: o-website-value('input-font-size-sm') !default; + +$input-padding-y-lg: o-website-value('input-padding-y-lg') !default; +$input-padding-x-lg: o-website-value('input-padding-x-lg') !default; +$input-font-size-lg: o-website-value('input-font-size-lg') !default; + +$input-border-width: o-website-value('input-border-width') !default; + +$input-border-radius: o-website-value('input-border-radius') !default; +$input-border-radius-lg: o-website-value('input-border-radius-lg') !default; +$input-border-radius-sm: o-website-value('input-border-radius-sm') !default; + +// Navbar + +// Increase default navbar padding for some navbar styles +$navbar-padding-y: if(index(('fill', 'pills', 'outline'), o-website-value('header-links-style')), ($spacer / 2) * 1.25, null) !default; +$navbar-nav-link-padding-x: if(index(('outline', 'block'), o-website-value('header-links-style')), .5rem * 3, null) !default; +$navbar-nav-link-padding-x: if(o-website-value('header-links-style') == 'border-bottom', .5rem * 2, null) !default; + +// Jumbotron + +$jumbotron-bg: transparent !default; + +// Bootstrap Review + +$o-btn-outline-defaults: () !default; +@each $color in ('primary', 'secondary') { + @if o-website-value('btn-#{$color}-outline') { + $o-btn-outline-defaults: append($o-btn-outline-defaults, $color); + } +} + +// Increase default navbar pills padding for 'pills' mode and add big radius +$o-navbar-nav-pills-link-padding-x: if(o-website-value('header-links-style') == 'pills', 1rem * 1.5, null) !default; +$o-navbar-nav-pills-link-border-radius: if(o-website-value('header-links-style') == 'pills', 10rem, null) !default; diff --git a/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss b/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss new file mode 100644 index 00000000..179584b7 --- /dev/null +++ b/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss @@ -0,0 +1,355 @@ +/** + * This file's purpose is to *ease* migration from 11.0. + */ + +// Restore gray utilities +$-compat-gray-map: ( + 'gray-darker': '900', + 'gray-dark': '900', + 'gray': '700', + 'gray-light': '600', + 'gray-lighter': '200', +) !default; +@each $old, $new in $-compat-gray-map { + @if not map-has-key($grays, $old) { + @include bg-variant(".bg-#{$old}", gray($new)); + @include text-emphasis-variant(".text-#{$old}", gray($new)); + } +} + +// Restore media ? + +// Restore progress bars +@each $color, $value in $theme-colors { + @include bg-variant(".progress-bar-#{$color}", $value); +} + +// Adapt radio ? + +// Adapt labels +.label { + @extend .badge; +} +@each $color, $value in $theme-colors { + .label-#{$color} { + @include badge-variant($value); + } +} +.label-default { + @include badge-variant(theme-color('secondary')); +} + +// Adapt center-block +.center-block { + display: block; + margin: auto; +} + +// Adapt pull-* classes +.pull-left { + float: left; +} +.pull-right { + float: right; +} + +// Adapt pagination +.pagination > li { + @extend .page-item; + > a { + @extend .page-link; + } +} + +// Adapt carousel +.carousel .item { + @extend .carousel-item; +} + +// Adapt checkboxes ? + +// Adapt tables +.table-condensed { + @extend .table-sm; +} + +// Adapt forms +.control-label { + @extend .col-form-label; +} +.help-block { + @extend .form-text; +} +.has-error .form-control { + @extend .is-invalid; +} +.has-success .form-control { + @extend .is-valid; +} +.form-horizontal .form-group { + @extend .row; +} + +// Adapt list-inline +.list-inline > li { + @extend .list-inline-item; +} + +// Adapt list-group +.panel .list-group { + @extend .list-group-flush; +} + +// Adapt image utilies +.img-rounded { + @extend .rounded; +} +.img-circle { + @extend .rounded-circle; +} + +// Adapt input group +.input-group { + .input-group-btn:first-child { + @extend .input-group-prepend; + } + .form-control ~ .input-group-btn { + @extend .input-group-append; + } + .input-group-addon { + @extend .input-group-append; + @extend .input-group-text; + } +} + +// Adapt panels +.panel { + @extend .card; +} +@each $color, $value in $theme-colors { + @include bg-variant(".panel-#{$color}", $value); +} +@include bg-variant(".panel-default", $white); +.panel-heading { + @extend .card-header; +} +.panel-body { + @extend .card-body; +} +.panel-footer { + @extend .card-footer; +} +.well { + @extend .card; + @extend .card-body; +} + +// Adapt grid (push-pull ?) +$-compat-breakpoints: ( + xs: map-get($grid-breakpoints, 'xs'), + sm: map-get($grid-breakpoints, 'md'), + md: map-get($grid-breakpoints, 'lg'), + lg: map-get($grid-breakpoints, 'xl'), +); +@each $breakpoint in map-keys($-compat-breakpoints) { + $infix: breakpoint-infix($breakpoint, $-compat-breakpoints); + $infix: if($infix != "", $infix, "-xs"); + + @include media-breakpoint-up($breakpoint, $-compat-breakpoints) { + // `$grid-columns - 1` because offsetting by the width of an entire row isn't possible + @for $i from 0 through ($grid-columns - 1) { + .col#{$infix}-offset-#{$i} { + @include make-col-offset($i, $grid-columns); + } + } + } +} + +// Adapt breadcrumb +.breadcrumb > li { + @extend .breadcrumb-item; +} + +// Adapt nav +.nav > li { + @extend .nav-item; + > a { + @extend .nav-link; + } +} +.nav-stacked { + flex-direction: column; +} +@include bg-variant(".navbar-default", $light); + +// Adapt img-responsive +.img-responsive { + @extend .img-fluid; +} + +// Adapt dropdowns +.dropdown-menu { + a { + @extend .dropdown-item; + } + .divider { + @extend .dropdown-divider; + } +} +.dropdown-toggle .caret { + display: none; +} + +// Adapt buttons +.btn-default { + @include button-variant(theme-color('secondary'), theme-color('secondary')); +} +.btn-xs { + @extend .btn-sm; +} + +// Adapt display classes +.hide { + display: none !important; +} +// The 'show' class could be supported if defined here and like that, +// unfortunately, BS4 still defines a 'show' class for other purposes which +// conflict with this (tab-pane, fade effects, ...). Adding more complex rules +// won't solve the problem as they would change css rules priorities. +// .show { +// display: block !important; +// } +.hidden { + display: none !important; +} +.visible { + &-xs, &-sm, &-md, &-lg { + &, &-block, &-inline, &-inline-block { + display: none !important; + } + } + &-xs { + &, &-block { + @include media-breakpoint-down(sm) { + display: block !important; + } + } + &-inline { + @include media-breakpoint-down(sm) { + display: inline !important; + } + } + &-inline-block { + @include media-breakpoint-down(sm) { + display: inline-block !important; + } + } + } + &-sm { + &, &-block { + @include media-breakpoint-only(md) { + display: block !important; + } + } + &-inline { + @include media-breakpoint-only(md) { + display: inline !important; + } + } + &-inline-block { + @include media-breakpoint-only(md) { + display: inline-block !important; + } + } + } + &-md { + &, &-block { + @include media-breakpoint-only(lg) { + display: block !important; + } + } + &-inline { + @include media-breakpoint-only(lg) { + display: inline !important; + } + } + &-inline-block { + @include media-breakpoint-only(lg) { + display: inline-block !important; + } + } + } + &-lg { + &, &-block { + @include media-breakpoint-up(xl) { + display: block !important; + } + } + &-inline { + @include media-breakpoint-up(xl) { + display: inline !important; + } + } + &-inline-block { + @include media-breakpoint-up(xl) { + display: inline-block !important; + } + } + } +} +.hidden { + &-xs { + @include media-breakpoint-down(sm) { + display: none !important; + } + } + &-sm { + @include media-breakpoint-only(md) { + display: none !important; + } + } + &-md { + @include media-breakpoint-only(lg) { + display: none !important; + } + } + &-lg { + @include media-breakpoint-up(xl) { + display: none !important; + } + } +} +.visible-print { + display: none !important; + + @media print { + display: block !important; + } + + &-block { + display: none !important; + + @media print { + display: block !important; + } + } + &-inline { + display: none !important; + + @media print { + display: inline !important; + } + } + &-inline-block { + display: none !important; + + @media print { + display: inline-block !important; + } + } +} +.hidden-print { + @media print { + display: none !important; + } +} diff --git a/addons/website/static/src/scss/options/colors/user_color_palette.scss b/addons/website/static/src/scss/options/colors/user_color_palette.scss new file mode 100644 index 00000000..7abaee4a --- /dev/null +++ b/addons/website/static/src/scss/options/colors/user_color_palette.scss @@ -0,0 +1,4 @@ + +$o-user-color-palette: map-merge($o-user-color-palette, o-map-omit(( + // -- hook -- +))); diff --git a/addons/website/static/src/scss/options/colors/user_theme_color_palette.scss b/addons/website/static/src/scss/options/colors/user_theme_color_palette.scss new file mode 100644 index 00000000..300db091 --- /dev/null +++ b/addons/website/static/src/scss/options/colors/user_theme_color_palette.scss @@ -0,0 +1,4 @@ + +$o-user-theme-color-palette: map-merge($o-user-theme-color-palette, o-map-omit(( + // -- hook -- +))); diff --git a/addons/website/static/src/scss/options/ripple_effect.scss b/addons/website/static/src/scss/options/ripple_effect.scss new file mode 100644 index 00000000..cff35300 --- /dev/null +++ b/addons/website/static/src/scss/options/ripple_effect.scss @@ -0,0 +1,28 @@ +@keyframes o-btn-ripple { + 100% { + opacity: 0; + transform: scale(2.5); + } +} + +.o_ripple_item { + display: none; + position: absolute; + z-index: -1; + border-radius: 100%; + opacity: .3; + background: currentColor; + pointer-events: none; + transform: scale(0); +} + +.o_js_ripple_effect { + transform-style: preserve-3d; + position: relative !important; + overflow: hidden !important; + + .o_ripple_item { + display: block; + animation: o-btn-ripple ease-in; + } +} diff --git a/addons/website/static/src/scss/options/user_values.scss b/addons/website/static/src/scss/options/user_values.scss new file mode 100644 index 00000000..3b6899ad --- /dev/null +++ b/addons/website/static/src/scss/options/user_values.scss @@ -0,0 +1,7 @@ +// This file is meant to be edited automatically by the user. The variables it +// contains should not be renamed otherwise it would break existing customers +// customizations. + +$o-user-website-values: map-merge($o-user-website-values, o-map-omit(( + // -- hook -- +))); diff --git a/addons/website/static/src/scss/primary_variables.scss b/addons/website/static/src/scss/primary_variables.scss new file mode 100644 index 00000000..5d1bd417 --- /dev/null +++ b/addons/website/static/src/scss/primary_variables.scss @@ -0,0 +1,409 @@ + +//------------------------------------------------------------------------------ +// Colors +//------------------------------------------------------------------------------ + +$o-base-color-palette: map-merge($o-base-color-palette, ( + 'body': $o-portal-default-body-bg, + 'menu': 1, // o_cc1 + 'menu-border-color': null, // Default to classes used on the template + 'header-boxed': '200', + 'footer': 5, // o_cc5 + 'copyright': 'black-15', +)); + +// By default, all user color palette values are null. Each null value is +// automatically replaced with corresponsing color of chosen color palette. +$o-user-color-palette: () !default; + +// By default, all user theme color palette values are null. Each null value +// is automatically replaced with corresponsing color of chosen theme color +// palette. +$o-user-theme-color-palette: () !default; + +$o-social-colors: ( + 'facebook': #3B5999, + 'twitter': #55ACEE, + 'linkedin': #0077B5, + 'google-plus': #DD4B39, + 'youtube': #ff0000, + 'github': #1a1e22, + 'instagram': #cf2872, + 'whatsapp': #25d366, + 'pinterest': #C8232C, +); + +$o-theme-figcaption-opacity: 0.6; + +$o-theme-generic-color-palettes: ( + ( + 'o-color-1': #984c46, + 'o-color-2': #23323b, + 'o-color-3': #eceae4, + 'o-color-4': #FFFFFF, + 'o-color-5': #16121f, + + 'menu': 3, + 'footer': 3, + ), + ( + 'o-color-1': #B99932, + 'o-color-2': #DED1C1, + 'o-color-3': #F5F5F5, + 'o-color-4': #FFFFFF, + 'o-color-5': #373737, + + 'menu': 5, + 'copyright': 4, + ), + ( + 'o-color-1': #f8882f, + 'o-color-2': #6a7c8f, + 'o-color-3': #fdf8ef, + 'o-color-4': #FFFFFF, + 'o-color-5': #212c39, + ), + ( + 'o-color-1': #6E7993, + 'o-color-2': #96848C, + 'o-color-3': #8F9AA2, + 'o-color-4': #D5D5D5, + 'o-color-5': #313347, + + 'menu': 5, + ), + ( + 'o-color-1': #F7CF41, + 'o-color-2': #1A2930, + 'o-color-3': #989898, + 'o-color-4': #FFFFFF, + 'o-color-5': #0B1612, + + 'menu': 3, + 'footer': 3, + ), + ( + 'o-color-1': #45859A, + 'o-color-2': #B57D4D, + 'o-color-3': #F5F5F5, + 'o-color-4': #FFFFFF, + 'o-color-5': #10273C, + + 'menu': 2, + 'footer': 2, + 'copyright': 5, + ), + ( + 'o-color-1': #1a547a, + 'o-color-2': #ddc76a, + 'o-color-3': #D6E6F1, + 'o-color-4': #FFFFFF, + 'o-color-5': #2b3442, + + 'o-cc5-link': 'o-color-4', + 'o-cc5-text': #9b9ba0, + + 'menu': 5, + 'footer': 5, + 'copyright': 3, + ), + ( + 'o-color-1': #763240, + 'o-color-2': #C19F7F, + 'o-color-3': #FFFFFF, + 'o-color-4': #EAEAEA, + 'o-color-5': #2F2F2F, + + 'o-cc4-headings': 'o-color-3', + 'o-cc4-link': 'o-color-3', + 'o-cc4-text': rgba(#fff, .8), + + 'o-cc5-headings': 'o-color-3', + 'o-cc5-link': 'o-color-3', + 'o-cc5-text': rgba(#fff, .8), + + 'footer': 1, + 'copyright': 4, + ), + ( + 'o-color-1': #4DC5C1, + 'o-color-2': #EC576B, + 'o-color-3': #E5E337, + 'o-color-4': #FFFFFF, + 'o-color-5': #000000, + + 'menu': 5, + ), + ( + 'o-color-1': #b56355, + 'o-color-2': #6ba17a, + 'o-color-3': #ebe6ea, + 'o-color-4': #FFFFFF, + 'o-color-5': #343733, + + 'footer': 2, + ), + ( + 'o-color-1': #01ACAB, + 'o-color-2': #FEDC3D, + 'o-color-3': #FAE8E0, + 'o-color-4': #FFFFFF, + 'o-color-5': #000000, + + 'footer': 1, + ), + ( + 'o-color-1': #926190, + 'o-color-2': #F3E0CD, + 'o-color-3': #F9EFE9, + 'o-color-4': #FFFFFF, + 'o-color-5': #291528, + + 'o-cc4-headings': 'o-color-4', + 'o-cc4-link': 'o-color-4', + 'o-cc4-text': rgba(#fff, .8), + + 'o-cc5-headings': 'o-color-4', + 'o-cc5-link': 'o-color-4', + 'o-cc5-text': rgba(#fff, .6), + ), + ( + 'o-color-1': #478FA2, + 'o-color-2': #CECECE, + 'o-color-3': #E8E9E9, + 'o-color-4': #FFFFFF, + 'o-color-5': #173F54, + + 'footer': 1, + 'copyright': 1, + ), + ( + 'o-color-1': #3CC37C, + 'o-color-2': #E9C893, + 'o-color-3': #F5F5F5, + 'o-color-4': #FFFFFF, + 'o-color-5': #1F3A2A, + + 'footer': 1, + 'copyright': 5, + ), + ( + 'o-color-1': #01524B, + 'o-color-2': #1993A3, + 'o-color-3': #dddde6, + 'o-color-4': #FFFFFF, + 'o-color-5': #011D1B, + + 'o-cc4-btn-primary': 'o-color-4', + 'o-cc4-link': 'o-color-4', + 'o-cc4-text': rgba(#fff, .8), + + 'o-cc5-btn-primary': 'o-color-4', + 'o-cc5-link': 'o-color-4', + 'o-cc5-text': rgba(#fff, .6), + + 'footer': 2, + 'copyright': 5, + ), + ( + 'o-color-1': #464D77, + 'o-color-2': #36827f, + 'o-color-3': #f2f0ec, + 'o-color-4': #FFFFFF, + 'o-color-5': #22263c, + + 'o-cc4-btn-primary': 'o-color-4', + 'o-cc4-link': 'o-color-4', + 'o-cc4-text': rgba(#fff, .8), + + 'o-cc5-btn-primary': 'o-color-4', + 'o-cc5-btn-secondary': #d6d4d0, + 'o-cc5-link': 'o-color-4', + 'o-cc5-text': rgba(#fff, .6), + + 'menu': 2, + 'footer': 2, + 'copyright': 5, + ), + ( + 'o-color-1': #4717f6, + 'o-color-2': #A43ACB, + 'o-color-3': #FAFAFA, + 'o-color-4': #FFFFFF, + 'o-color-5': #0F0A19, + + 'menu': 5, + 'footer': 5, + ), +); + +//------------------------------------------------------------------------------ +// Website customizations +//------------------------------------------------------------------------------ + +$o-base-website-values-palette: ( + 'font-size-base': 1rem, // Need a set value as the value is used in bootstrap_overridden files + 'google-fonts': null, + + 'body-image': null, + 'body-image-type': 'image', // 'image' or 'pattern' + + 'layout': 'full', // 'full' / 'boxed' + 'color-palettes-number': null, // Default to the individual variables for each color palette type + + 'btn-primary-outline': false, + 'btn-secondary-outline': false, + 'link-underline': 'hover', // 'never' / 'hover' / 'always' + 'btn-ripple': false, + + 'btn-padding-y': null, // Default to BS + 'btn-padding-x': null, // Default to BS + 'btn-font-size': null, // Default to BS + 'btn-padding-y-sm': null, // Default to portal value + 'btn-padding-x-sm': null, // Default to portal value + 'btn-font-size-sm': null, // Default to BS + 'btn-padding-y-lg': null, // Default to BS + 'btn-padding-x-lg': null, // Default to BS + 'btn-font-size-lg': null, // Default to BS + 'btn-border-width': null, // Default to BS + 'btn-border-radius': null, // Default to BS + 'btn-border-radius-sm': null, // Default to BS + 'btn-border-radius-lg': null, // Default to BS + + 'input-padding-y': null, // Default to BS + 'input-padding-x': null, // Default to BS + 'input-font-size': null, // Default to BS + 'input-padding-y-sm': null, // Default to BS + 'input-padding-x-sm': null, // Default to BS + 'input-font-size-sm': null, // Default to BS + 'input-padding-y-lg': null, // Default to BS + 'input-padding-x-lg': null, // Default to BS + 'input-font-size-lg': null, // Default to BS + 'input-border-width': null, // Default to BS + 'input-border-radius': null, // Default to BS + 'input-border-radius-sm': null, // Default to BS + 'input-border-radius-lg': null, // Default to BS + + // A key from the $o-theme-font-configs map (null = default to the first key) + 'font': null, + 'headings-font': null, + 'navbar-font': null, + 'buttons-font': null, + + 'header-template': 'default', // 'default' / 'hamburger' / 'vertical' / 'sidebar' + 'header-font-size': null, // Default to BS (normal font-size) + 'header-links-style': 'default', // 'default' / 'fill' / 'outline' / 'pills' / 'block' / 'border-bottom' + 'logo-height': null, // Default to navbar height (see portal) + 'hamburger-type': 'default', // 'default' / 'off-canvas' + 'hamburger-position': 'left', // 'left' / 'center' / 'right' + 'menu-border-width': null, // Default to classes used on the template + 'menu-border-style': solid, // Default to classes used on the template + 'menu-border-radius': null, // Default to classes used on the template + 'menu-box-shadow': null, // Default to classes used on the template + 'sidebar-width': 18.75rem, // 300px + + 'footer-template': 'default', + 'footer-effect': null, // null / 'slideout_slide_hover' / 'slideout_shadow' + 'footer-scrolltop': false, +); +$o-font-aliases-to-keys: ( + 'base': 'font', + 'headings': 'headings-font', + 'navbar': 'navbar-font', + 'buttons': 'buttons-font', +); +$o-website-values-palettes: ( + ( + 'headings-font': 'Source Sans Pro', + 'navbar-font': 'Source Sans Pro', + 'buttons-font': 'Source Sans Pro', + ), +) !default; +$o-website-values-palette-number: 1 !default; + +// By default, all user website values are null. Each null value is +// automatically replaced with corresponsing value of chosen values palette. +$o-user-website-values: () !default; + +//------------------------------------------------------------------------------ +// Fonts +//------------------------------------------------------------------------------ + +// Those are BS values, except BS hardcodes them inside the $hx-font-size +// variables directly and don't make them customizable. +$o-theme-h1-font-size-multiplier: 2.5 !default; +$o-theme-h2-font-size-multiplier: 2 !default; +$o-theme-h3-font-size-multiplier: 1.75 !default; +$o-theme-h4-font-size-multiplier: 1.5 !default; +$o-theme-h5-font-size-multiplier: 1.25 !default; +$o-theme-h6-font-size-multiplier: 1 !default; + +// Map: +// <font-name>: ( +// 'family': <css font family list>, +// 'url': <related part of google fonts URL>, +// 'properties' (optional): ( +// <font-alias>: ( +// <website-value-key>: <value>, +// ..., +// ), +// ..., +// ) +// ) +$o-theme-font-configs: ( + 'Roboto': ( + 'family': ('Roboto', sans-serif), + 'url': 'Roboto:300,300i,400,400i,700,700i', + ), + 'Open Sans': ( + 'family': ('Open Sans', sans-serif), + 'url': 'Open+Sans:300,300i,400,400i,700,700i', + ), + 'Source Sans Pro': ( + 'family': ('Source Sans Pro', sans-serif), + 'url': 'Source+Sans+Pro:300,300i,400,400i,700,700i', + ), + 'Raleway': ( + 'family': ('Raleway', sans-serif), + 'url': 'Raleway:300,300i,400,400i,700,700i', + ), + 'Noto Serif': ( + 'family': ('Noto Serif', serif), + 'url': 'Noto+Serif:300,300i,400,400i,700,700i', + ), + 'Arvo': ( + 'family': ('Arvo', Times, serif), + 'url': 'Arvo:300,300i,400,400i,700,700i', + ), +) !default; + +//------------------------------------------------------------------------------ +// Mixins +//------------------------------------------------------------------------------ + +@mixin o-ribbon-right() { + @include o-position-absolute($top: 0, $right: 0); + padding: 0.5rem $ribbon-padding; + // 0.708 is 1 - cos(45deg) + // Transforms are applied right-to-left + // Cannot use matrix because of the use of % values. + transform: translateX(calc(-0.708 * (100% - #{2 * $ribbon-padding}))) rotate(45deg) translateX(calc(100% - #{$ribbon-padding})); + transform-origin: top right; +}; + +@mixin o-ribbon-left() { + @include o-position-absolute($top: 0, $left: 0); + padding: 0.5rem $ribbon-padding; + transform: translateX(calc(0.708 * (100% - #{2 * $ribbon-padding}) - 100%)) rotate(-45deg) translateX($ribbon-padding); + transform-origin: top right; +}; + +@mixin o-tag-right() { + @include o-position-absolute($top: 0, $right: 0); + padding: 0.25rem 1rem; +}; + +@mixin o-tag-left() { + @include o-position-absolute($top: 0, $left: 0); + padding: 0.25rem 1rem; +}; diff --git a/addons/website/static/src/scss/secondary_variables.scss b/addons/website/static/src/scss/secondary_variables.scss new file mode 100644 index 00000000..53f8f9ed --- /dev/null +++ b/addons/website/static/src/scss/secondary_variables.scss @@ -0,0 +1,194 @@ +//------------------------------------------------------------------------------ +// Website customizations +//------------------------------------------------------------------------------ + +// Complete the base website values palette with the first defined font +$-first-font-name: nth(map-keys($o-theme-font-configs), 1); +@each $alias, $key in $o-font-aliases-to-keys { + @if map-get($o-base-website-values-palette, $key) == null { + $o-base-website-values-palette: map-merge($o-base-website-values-palette, ( + $key: $-first-font-name, + )); + } +} + +@function o-add-font-config($values) { + @each $alias, $key in $o-font-aliases-to-keys { + $font-name: map-get($values, $key); + $font-config: o-safe-get($o-theme-font-configs, $font-name, ()); + $font-properties: o-safe-get($font-config, 'properties', ()); + $type-font-properties: o-safe-get($font-properties, $alias, ()); + $values: map-merge($values, $type-font-properties); + } + @return $values; +} + +// Some fonts have been renamed in a stable version, and for retro compatibility +// for users which have a custom user_values.css as attachment with an old font +// already used, we map the old font with the new `similar` font +$o-fonts-similar: ( + 'Droid Serif': 'Noto Serif', + 'SinKinSans': 'Spartan', + 'Proxima': 'Montserrat', + 'Comic Sans MS': 'Comic Neue', + 'Fontastique': 'Bubblegum Sans', + 'Luminari': 'Eagle Lake', + 'Fecske': 'Marcellus', + 'Din Alternate': 'Roboto', +); + +@function o-map-font-aliases($values) { + $-values: $values; + @each $key in map-values($o-font-aliases-to-keys) { + $value: map-get($values, $key); + @if ($value and map-has-key($o-fonts-similar, $value)) { + $-values: map-merge($-values, ( + $key: map-get($o-fonts-similar, $value), + )); + } + } + @return $-values; +}; + +// By default, most website palette values are null. Each null value is +// automatically replaced with corresponsing values in chosen default values +// palette. +$-website-values-default: o-safe-nth($o-website-values-palettes, $o-website-values-palette-number, ()); +$-website-values-default: map-merge($o-base-website-values-palette, o-map-omit($-website-values-default)); +$o-user-website-values: o-map-font-aliases(o-map-omit($o-user-website-values)); +$-actual-user-website-values-palette: map-merge($-website-values-default, $o-user-website-values); +// Default font selection + User font selection have been merged, now need to +// add the right associated font default config +$-actual-user-website-values-palette: o-add-font-config($-actual-user-website-values-palette); +// Reforce the properties which already had a set values in the user map (the +// font properties override the default palette values but not the user ones) +$-actual-user-website-values-palette: map-merge($-actual-user-website-values-palette, $o-user-website-values); +$o-website-values-palettes: append($o-website-values-palettes, $-actual-user-website-values-palette); + +// Enable last website values palette, which is now the user customized one +$o-website-values-palette-number: length($o-website-values-palettes); +$o-website-values: $-actual-user-website-values-palette !default; +@function o-website-value($key) { + @return map-get($o-website-values, $key); +} + +$o-theme-navbar-logo-height: o-website-value('logo-height') !default; +$o-theme-navbar-fixed-logo-height: o-website-value('fixed-logo-height') !default; + +//------------------------------------------------------------------------------ +// Colors +//------------------------------------------------------------------------------ + +// First change the palette number to the actual user choice if any. +$-color-palettes-number: o-website-value('color-palettes-number'); +@if $-color-palettes-number { + $o-color-palette-number: $-color-palettes-number; + $o-theme-color-palette-number: $-color-palettes-number; + $o-gray-color-palette-number: $-color-palettes-number; +} + +$o-has-customized-13-0-color-system: + not not (map-get($o-user-theme-color-palette, 'primary') + or map-get($o-user-theme-color-palette, 'secondary') + or map-get($o-user-theme-color-palette, 'alpha') + or map-get($o-user-theme-color-palette, 'beta') + or map-get($o-user-theme-color-palette, 'gamma') + or map-get($o-user-theme-color-palette, 'delta') + or map-get($o-user-theme-color-palette, 'epsilon')); + +$o-has-customized-colors: + not not (length(map-keys(o-map-omit($o-user-color-palette))) > 0 + or map-get($o-user-theme-color-palette, 'success') + or map-get($o-user-theme-color-palette, 'info') + or map-get($o-user-theme-color-palette, 'warning') + or map-get($o-user-theme-color-palette, 'danger')); + +// Color palette +// ------------- + +// Add generic color palettes +$o-color-palettes: join($o-color-palettes, $o-theme-generic-color-palettes); + +// By default, most user color palette values are null. Each null value is +// automatically replaced with corresponsing colors in chosen default color +// palette. +$-palette-default: o-safe-nth($o-color-palettes, $o-color-palette-number, ()); +$-actual-user-color-palette: map-merge($-palette-default, o-map-omit($o-user-color-palette)); +$o-color-palettes: append($o-color-palettes, $-actual-user-color-palette); + +// Theme color palette +// ------------------- + +// alpha -> epsilon colors are from the old color system, this is kept for +// compatibility: Generate default theme color scheme if alpha is set +$-alpha: map-get($o-user-theme-color-palette, 'alpha'); +@if ($-alpha) { + $o-user-theme-color-palette: map-merge(( + beta: lighten(desaturate($-alpha, 60%), 30%), + gamma: desaturate(adjust-hue($-alpha, -45deg), 10%), + delta: desaturate(adjust-hue($-alpha, 45deg), 10%), + epsilon: desaturate(adjust-hue($-alpha, 180deg), 10%), + ), o-map-omit($o-user-theme-color-palette)); +} + +// By default, all user theme color palette values are null. Each null value is +// automatically replaced with corresponsing colors in chosen default theme +// color palette. +$-palette-default: o-safe-nth($o-theme-color-palettes, $o-theme-color-palette-number, ()); +$-actual-user-theme-color-palette: map-merge($-palette-default, o-map-omit($o-user-theme-color-palette)); +// Always remove the primary/secondary which were customizable in some theme +// in Odoo <= 13.3. The customer can always rechoose the right color in the +// Odoo color system as the first two ones are mapped to primary/secondary. +$-actual-user-theme-color-palette: map-remove($-actual-user-theme-color-palette, + 'primary', + 'secondary' +); +$o-theme-color-palettes: append($o-theme-color-palettes, $-actual-user-theme-color-palette); + +// --- + +// Enable last color and theme color palettes, which are now the user customized +// color palettes. +$o-original-color-palette-number: $o-color-palette-number; +$o-color-palette-number: length($o-color-palettes); +$o-theme-color-palette-number: length($o-theme-color-palettes); + +$o-we-auto-contrast-exclusions: () !default; +$o-we-auto-contrast-exclusions: join($o-we-auto-contrast-exclusions, map-keys(o-map-omit($o-user-color-palette))); + +//------------------------------------------------------------------------------ +// Fonts +//------------------------------------------------------------------------------ + +// Merge base fonts with user-added google fonts +@each $font-name in (o-website-value('google-fonts') or ()) { + $o-theme-font-configs: map-merge($o-theme-font-configs, ( + $font-name: ( + 'family': (quote($font-name), sans-serif), + 'url': quote($font-name) + ':300,300i,400,400i,700,700i', + ), + )); +} + +// Add odoo unicode support for all fonts +@each $font-name, $font-config in $o-theme-font-configs { + $o-theme-font-configs: map-merge($o-theme-font-configs, ( + $font-name: map-merge($font-config, ( + 'family': o-add-unicode-support-font(map-get($font-config, 'family')), + )), + )); +} + +// Function which allows to retrieve a base info (family, url, properties) about +// a component (base, navbar, ...)'s font. The font name is retrievable via a +// simple o-website-value call. +@function o-get-font-info($alias: 'base', $config-key: 'family') { + $key: map-get($o-font-aliases-to-keys, $alias); + $font-name: o-website-value($key); + $-font-config: o-safe-get($o-theme-font-configs, $font-name, ()); + @return map-get($-font-config, $config-key); +} +$o-theme-font: o-get-font-info('base') or (sans-serif,) !default; +$o-theme-headings-font: o-get-font-info('headings') or $o-theme-font !default; +$o-theme-navbar-font: o-get-font-info('navbar') or $o-theme-font !default; +$o-theme-buttons-font: o-get-font-info('buttons') or $o-theme-font !default; diff --git a/addons/website/static/src/scss/user_custom_bootstrap_overridden.scss b/addons/website/static/src/scss/user_custom_bootstrap_overridden.scss new file mode 100644 index 00000000..ecf5d460 --- /dev/null +++ b/addons/website/static/src/scss/user_custom_bootstrap_overridden.scss @@ -0,0 +1,22 @@ +// +// /!\ +// This file is meant to regroup your bootstrap customizations. In that file, +// you must define variables *ONLY*. If you want to introduce new CSS rules +// for your website, check the 'user_custom_rules.scss' file you can also edit. +// /!\ +// +// You can change the value of a variable you can find in the bootstrap 4 +// documentation (or in the file /web/static/lib/bootstrap/scss/_variables.scss) +// and Odoo will automatically adapt its design to your new bootstrap. For +// example, doing this will make some shadows and gradients appear, especially +// for your buttons design: +// +// $enable-shadows: true; +// $enable-gradients: true; +// +// Notice that Odoo already overrides bootstrap variables according to your +// choices in the "Customize Theme" dialog, you should first take a look at +// it and do customizations this way. Indeed, if you overridde the same +// variables, Odoo will either have to ignore them or not be able to make +// the "Customize Theme" dialog work for these variables anymore. +// diff --git a/addons/website/static/src/scss/user_custom_rules.scss b/addons/website/static/src/scss/user_custom_rules.scss new file mode 100644 index 00000000..181c4c17 --- /dev/null +++ b/addons/website/static/src/scss/user_custom_rules.scss @@ -0,0 +1,8 @@ +// +// This file is meant to regroup your design customizations. For example, doing +// this will separate your footer with a dotted border using your primary color. +// +// footer { +// border-top: 5px dotted theme-color('primary'); +// } +// diff --git a/addons/website/static/src/scss/website.backend.scss b/addons/website/static/src/scss/website.backend.scss new file mode 100644 index 00000000..1122930e --- /dev/null +++ b/addons/website/static/src/scss/website.backend.scss @@ -0,0 +1,452 @@ +.o_dashboards { + background-color: #ececec; + + .o_website_dashboard { + background-color: #ececec; + div.o_box { + @include clearfix; + color: $o-main-color-muted; + background-color: $o-view-background-color; + background-size: cover; + margin-top: $o-horizontal-padding; + position: static; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3); + + h2, h4 { + text-transform: uppercase; + margin-top: 0; + color: $o-brand-odoo; + } + + h4 { margin: 0 0 0 8px; } + + .col-lg-7, .col-12 { + padding: 15px; + + .js_field_selection { + width: 30%; + margin: 0 0 20px 0; + float: right; + border-radius: 0; + } + + .table-responsive { + border: none; + } + + table { + + tr:first-child { + background: white; + } + + tr:nth-child(even):not(:hover) { + background: #f5f6f7; + } + + th { + text-transform: uppercase; + color: $o-main-text-color; + border-top-width: 0px; + } + td, th { + text-align: right; + border-left: none; + + &:first-child { + text-align: left; + } + } + .o_tooltip_key { + text-align: left; + } + } + } + } + + .o_dashboard_common { + .o_box { + display: flex; + flex-flow: row wrap; + justify-content: flex-start; + > .o_inner_box { + @include media-breakpoint-down(sm) { + flex: 1 1 200px; + display: block !important; + } + @include media-breakpoint-up(md) { + flex: 0 0 16.6%; + } + } + } + .o_inner_box { + padding-top: 10px; + text-align: center; + border: 1px solid $o-view-background-color; + height: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + color: white; + background-color: $o-brand-primary; + &:hover { + background-color: darken($o-brand-primary, 10%); + } + &.o_primary { + background-color: $o-brand-odoo; + color: white; + &:hover { + background-color: darken($o-brand-odoo, 10%); + } + } + .o_highlight { + font-size: 27px; + } + } + } + + .o_graph_sales { + direction: ltr#{"/*rtl:ignore*/"}; + .o_legend0 { + background-color: $o-brand-primary; + } + .o_legend1 { + background-color: $o-main-color-muted; + } + } + + .o_dashboard_visits { + + h2 { + padding: 15px; + } + + .o_demo_background { + + margin-top: 16px; + height: 300px; + background-size: 100% !important; + background: url("/website/static/src/img/website_dashboard_visit_demo.png") no-repeat; + position: relative; + + .o_buttons { + position: relative; + + > button { + display: block; + } + } + + .o_layer { + background-color: rgba(255,255,255,.3); + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + + .o_properties_selection { + + @include media-breakpoint-up(lg) { + display: flex; + } + + margin-top: 10px; + margin-bottom: 10px; + } + + .gapi-analytics-auth-styles-signinbutton { + cursor: pointer; + } + + .ActiveUsers { + position: relative; + float: right; + right: 10px; + border: 1px solid gray('200'); + font-weight: 300; + white-space: nowrap; + padding: .5em 1.5em; + margin: .5em; + text-transform: uppercase; + } + + .ActiveUsers-value { + font-weight: 300; + margin-right: -.25em; + } + + .ActiveUsers.is-increasing { + animation: increase 3s; + } + + .ActiveUsers.is-decreasing { + animation: decrease 3s; + } + + @keyframes increase { + 10% { + background-color: lighten($o-brand-primary, 30%); + border-color: $o-brand-primary; + color: $o-brand-primary; + } + } + + @keyframes decrease { + 10% { + background-color: lighten($o-brand-primary, 30%); + border-color: $o-brand-primary; + color: $o-brand-primary; + } + } + } + + tr.o_product_template { + cursor: pointer; + color: $o-main-text-color; + } + + .o_loader { + text-align: center; + width: 100%; + height: 20em; + } + } +} + +.oe_stat_button { + &.o_stat_button_info:hover { + color: #666666 !important; + background-color: transparent !important; + opacity: 0.8 !important; + cursor: default !important; + } +} + +.o_kanban_view.o_theme_kanban { + $o-theme-kanban-gray: #fcfcfc; + background-color: $o-theme-kanban-gray; + + /// Un-grouped Layout (default) + &.o_kanban_ungrouped { + justify-content: space-between; + margin: 0 0 0 ($o-kanban-record-margin - 2 * $grid-gutter-width); + + @include media-breakpoint-down(sm) { + padding-left: $o-horizontal-padding - $o-kanban-record-margin; + padding-right: $o-horizontal-padding; + } + + .o_kanban_record { + flex: 1 0 220px; + } + } + + .o_kanban_record { + margin-left: $grid-gutter-width * 2; + padding: 0; + box-shadow: none; + background: none; + border: none; + + .o_theme_preview_top { + position: relative; + border: 1px solid darken($o-theme-kanban-gray, 20%); + border-radius: 2px; + transform-origin: center bottom; + transition: all .1s ease 0s; + + .o_theme_cover, .o_theme_logo, .o_theme_screenshot { + width: 100%; + padding-bottom: 127%; + background-repeat: no-repeat; + background-position: center top; + background-size: 40% 32%; + } + + .o_theme_cover { + background-size: contain; + } + + .o_theme_screenshot { + background-size: cover; + } + } + + .o_theme_preview_bottom { + h5, h6 { + line-height: 16px; + } + } + + .o_button_area { + @include o-position-absolute(0, 0, 0, 0); + transition: opacity 100ms ease 0s; + display: flex; + flex-flow: column nowrap; + justify-content: center; + transform: translate3d(0,0,0); + background-image: linear-gradient(0deg, rgba(black, 0.6), rgba(black, 0.3)); + padding: 10% 20%; + opacity: 0; + visibility: hidden; + + > .btn { + padding: $btn-padding-y-lg $btn-padding-x-lg; + } + + hr { + width: 100%; + } + } + + .o_theme_preview_top:hover { + transition: all .3s ease 0s; + transform: translateY(-10px); + border-color: darken($o-theme-kanban-gray, 26%); + box-shadow: 0 15px 12px -8px rgba(0, 0, 0, .4); + + .o_theme_screenshot { + animation: o_theme_screenshot_scroll 4s linear .25s infinite alternate; + } + @keyframes o_theme_screenshot_scroll { + 25% { + background-position: center top; + } + 75%, 100% { + background-position: center bottom; + } + } + + .o_button_area { + opacity: 1; + visibility: visible; + transition: opacity 0.2s ease 0.1s; + } + } + + &.o_theme_installed .o_theme_preview_top { + border: 3px solid $o-brand-primary; + } + } + + /// Grouped Layout + &.o_kanban_grouped { + .o_kanban_group { + background-color: $o-theme-kanban-gray; + padding: 0 20px; + + .o_kanban_header { + height: 30px; + + .o_column_title { + padding: 0; + color: $body-color; + } + + &:hover, &.show { + .o_kanban_config { + display: none; + } + } + } + + .o_theme_preview_top { + border-color: darken($o-theme-kanban-gray, 16%); + } + + &:nth-child(even) { + background-color: darken($o-theme-kanban-gray, 4%); + + .o_theme_cover, .o_theme_logo { + background-color: white; + } + } + } + + .o_kanban_record { + width: 100%; + margin-left: 0; + margin-right: 0; + + .o_theme_preview_top { + .o_theme_cover, .o_theme_logo { + padding-bottom: 50%; + background-size: 32% 62%; + } + + .o_theme_cover { + background-size: cover; + } + } + } + } +} + +.o_view_form_theme_preview_controller { + .o_control_panel > div:first-of-type { + display: none; + } + div.o_form_nosheet { + padding: 0px; + height:100%; + width:100%; + } + + .is_mobile { + @include media-breakpoint-up(md) { + iframe { + // mobile frame is rounded + border-radius: 15px; + height: 735px; + } + .img_mobile { + pointer-events: none; + display: block !important; + position: absolute; + top: 16px; + left: calc(50% - 200px) + } + .o_field_iframe_preview { + margin: auto !important; + padding: 53px 11px 58px 28px; + width: 416px; + } + } + } + +} +.o_view_kanban_theme_preview_controller { + .o_control_panel > div:nth-child(2) { + display: none; + } +} + + +.o_theme_install_loader_container { + background-color: rgba($o-shadow-color, .9); + pointer-events: all; + font-size: 4.5rem; + justify-content: space-evenly; + .o_tooltip { + top: 0 !important; + left: 0 !important; + margin-right: 7px !important; + padding-left: 28px !important; + } + .o_theme_install_loader_tip { + font-size: 0.5em; + } +} +.o_theme_install_loader { + position: relative; + display: inline-block; + width: 400px; + height: 220px; + background-image: url('/website/static/src/img/theme_loader.gif'); + background-size: cover; + border-radius: 6px; +} diff --git a/addons/website/static/src/scss/website.edit_mode.scss b/addons/website/static/src/scss/website.edit_mode.scss new file mode 100644 index 00000000..33d7e7ed --- /dev/null +++ b/addons/website/static/src/scss/website.edit_mode.scss @@ -0,0 +1,220 @@ +$-editor-messages-margin-x: 2%; +%o-editor-messages { + width: 100% - $-editor-messages-margin-x * 2; // Need to be forced here to avoid flickering + margin: 20px $-editor-messages-margin-x; + border: 2px dashed #999999; + padding: 12px 0px; + text-align: center; + color: #999999; + + &:before { + content: attr(data-editor-message); + display: block; + font-size: 20px; + line-height: 50px; // Useful for the "wizz" animation on snippet click to be more visible + } + &:after { + content: attr(data-editor-sub-message); + display: block; + } +} +.o_we_snippet_area_animation { + animation-delay: 999ms; // Disable it but allow to inherit the animation + + &::before { + animation: inherit; + animation-delay: 0ms; + } +} + +.o_editable { + &:not(:empty), &[data-oe-type] { + &:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"]):not(.o_editable_no_shadow):not([data-oe-type="image"]):hover, + &.o_editable_date_field_linked { + box-shadow: $o-brand-odoo 0 0 5px 2px inset; + } + &[data-oe-type="image"]:not(.o_editable_no_shadow):hover { + position: relative; + + &:after { + content: ""; + pointer-events: none; + @include o-position-absolute(0, 0, 0, 0); + z-index: 1; + box-shadow: $o-brand-odoo 0 0 5px 2px inset; + } + } + } + &:focus, &[data-oe-type] { + min-height: 0.8em; + min-width: 8px; + + // TODO this feature just needs to be reviewed to not have to make + // exceptions such as this + &#o_footer_scrolltop_wrapper { + min-height: 0; + min-width: 0; + } + } + &.o_is_inline_editable { + display: inline-block; + } + .btn, &.btn { + -webkit-user-select: auto; + -moz-user-select: auto; + -ms-user-select: auto; + user-select: auto; + cursor: text!important; + } + /* Summernote not Support for placeholder text https://github.com/summernote/summernote/issues/581 */ + &[placeholder]:empty:not(:focus):before { + content: attr(placeholder); + opacity: 0.3; + } + + &.oe_structure.oe_empty, &[data-oe-type=html], .oe_structure.oe_empty { + &#wrap:empty, &#wrap > .oe_drop_zone.oe_insert:not(.oe_vertical):only-child { + @extend %o-editor-messages; + padding: 112px 0px; + } + > .oe_drop_zone.oe_insert:not(.oe_vertical):only-child { + @extend %o-editor-messages; + height: auto; + color: $o-brand-odoo; + } + > p:empty:only-child { + color: #aaa; + } + } +} +.editor_enable [data-oe-readonly]:hover { + cursor: default; +} +.oe_structure_solo > .oe_drop_zone { + // TODO implement something more robust. This is currently for our only + // use case of oe_structure_solo: the footer. The dropzone in there need to + // be 1px lower that the end-of-page dropzone to distinguish them. The + // usability has to be reviewed anyway. + transform: translateY(10px); // For some reason "1px" is not enough... +} + +/* Prevent the text contents of draggable elements from being selectable. */ +[draggable] { + user-select: none; +} + +.oe_editable:focus, +.css_editable_hidden, +.editor_enable .css_editable_mode_hidden { + outline: none!important; +} + +.editor_enable .css_non_editable_mode_hidden, +.o_editable .media_iframe_video .css_editable_mode_display { + display: block!important; +} + +// TODO: in master check if the class / rule is relevant at all +.editor_enable [data-oe-type=html].oe_no_empty:empty { + height: 16px!important; +} + +// EDITOR BAR +table.editorbar-panel { + cursor: pointer; + width: 100%; + td { border: 1px solid #aaa} + td.selected { background-color: #b1c9d9} +} +.link-style { + .dropdown > .btn { + min-width: 160px; + } + .link-style { + display: none; + } + li { + text-align: center; + label { + width: 100px; + margin: 0 5px; + } + } + .col-md-2 > * { + line-height: 2em; + } +} + +// fontawesome +.note-editable .fa { + cursor: pointer; +} + +// parallax dropzones are in conflict with outside of parallax dropzones +.parallax .oe_structure > .oe_drop_zone { + &:first-child { + top: 16px; + } + &:last-child { + bottom: 16px; + } +} + +.editor_enable .o_add_language { + display: none !important; +} + +// Facebook Page +.editor_enable .o_facebook_page:not(.o_facebook_preview) { + iframe { + pointer-events: none; + } + .o_facebook_alert .o_add_facebook_page { + cursor: pointer; + } +} + +body.editor_enable.editor_has_snippets { + padding-top: 0 !important; + + .s_popup .modal { + // s_popup in edit mode + background-color: transparent; + + &.fade .modal-dialog { + transform: none; + } + } + + #oe_main_menu_navbar + #wrapwrap .o_header_affixed { + top: 0; + } +} + +.editor_has_snippets { + .o_header_affixed { + right: $o-we-sidebar-width !important; + } +} + +.editor_enable { + @if o-website-value('header-template') == 'sidebar' { + #wrapwrap > header { + @if o-website-value('hamburger-position') != 'right' { + right: $o-we-sidebar-width; + } + } + } +} + +//s_dynamic_snippet +body.editor_enable { + .s_dynamic { + > * { + pointer-events: none; + } + [data-url] { + cursor: inherit; + } + } +} diff --git a/addons/website/static/src/scss/website.editor.ui.scss b/addons/website/static/src/scss/website.editor.ui.scss new file mode 100644 index 00000000..ebf94697 --- /dev/null +++ b/addons/website/static/src/scss/website.editor.ui.scss @@ -0,0 +1,79 @@ + +.o_homepage_editor_welcome_message { + padding-top: 128px; + padding-bottom: 128px; + font-family: Roboto, $font-family-sans-serif; +} + +// INPUTS +$o-we-switch-size: 2ex !default; +$o-we-switch-inactive-color: #F7F7F7 !default; +.o_switch { + display: flex; + align-items: center; + font-weight: normal; + cursor: pointer; + + > input { + display: none; + + + span { + background-color: $o-we-switch-inactive-color; + box-shadow: inset 0 0 0px 1px darken($o-we-switch-inactive-color, 10%); + border-radius: 100rem; + height: $o-we-switch-size; + width: $o-we-switch-size * 1.8; + margin-right: 0.5em; + display: inline-block; + transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1); + + &:after { + content: ""; + background: white; + display: block; + width: $o-we-switch-size - 0.2; + height: $o-we-switch-size - 0.2; + margin-top: 0.1ex; + margin-left: 0.1ex; + border-radius: 100rem; + transition: all 0.3s cubic-bezier(0.19, 1, 0.22, 1); + box-shadow: 0 1px 1px darken($o-we-switch-inactive-color, 35%), inset 0 0 0 1px lighten($o-we-switch-inactive-color, 10%); + } + } + + &:checked+span { + box-shadow: none; + background: $o-we-color-success; + + &:after { + margin-left: ($o-we-switch-size*1.8 - $o-we-switch-size) + 0.1; + } + } + } + + &.o_switch_danger { + >input { + &:not(:checked)+span { + box-shadow: none; + background: $o-we-color-danger; + } + } + } +} + +.o_new_content_loader_container { + background-color: rgba($o-shadow-color, .9); + pointer-events: all; + font-size: 3.5rem; + justify-content: center; + z-index: $zindex-modal - 1; +} +.o_new_content_loader { + position: relative; + display: inline-block; + width: 400px; + height: 220px; + background-image: url('/website/static/src/img/theme_loader.gif'); + background-size: cover; + border-radius: 6px; +} diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss new file mode 100644 index 00000000..06627a58 --- /dev/null +++ b/addons/website/static/src/scss/website.scss @@ -0,0 +1,1437 @@ +/// +/// This file regroups the website design rules. +/// + +$-seen-urls: (); +@each $alias, $key in $o-font-aliases-to-keys { + $-url: o-get-font-info($alias, 'url'); + @if $-url and index($-seen-urls, $-url) == null { + $-seen-urls: append($-seen-urls, $-url); + @import url("https://fonts.googleapis.com/css?family=#{unquote($-url)}&display=swap"); + } +} + +:root { + // The theme customize modal JS will need to know the value of some scss + // variables used to render the user website, and those may have been + // customized by themes, the user or anything else (so there is no file to + // parse to get them). Those will be printed here as CSS variables. + + @include print-variable('support-13-0-color-system', $o-support-13-0-color-system); + @include print-variable('has-customized-13-0-color-system', $o-has-customized-13-0-color-system); + + // 1) Handle default values + @include print-variable('header-font-size', $font-size-base); + + // 2) The values in the $theme-colors map are already printed by Bootstrap. + + // 3) The values in the $colors map are also printed by Bootstrap. However, + // we have color variables which can contain a reference to a color + // combination and that is the info we want in that case. As a stable fix, + // we'll leave the original variable untouched but print a prefixed version + // of the variable with the correct reference value. + // TODO adapt in master + @each $key in ('menu', 'header-boxed', 'footer', 'copyright') { + $-value: map-get($o-color-palette, $key); + @if type-of($-value) == 'number' { + @include print-variable('bugfixed-#{$key}', $-value); + } + } + + // 4) The Odoo values map, $o-website-values, must be printed. + @each $key, $value in $o-website-values { + @include print-variable($key, $value); + } + + // 5) Use final value used by the theme + @include print-variable('body', $body-bg); + + @include print-variable('logo-height', $o-theme-navbar-logo-height); + @include print-variable('fixed-logo-height', $o-theme-navbar-fixed-logo-height); + + $-font-names: map-keys($o-theme-font-configs); + @include print-variable('number-of-fonts', length($-font-names)); + $i: 1; + @each $font-name in $-font-names { + @include print-variable('font-number-#{$i}', $font-name); + $i: $i + 1; + } + + @include print-variable('btn-padding-y', $btn-padding-y); + @include print-variable('btn-padding-x', $btn-padding-x); + @include print-variable('btn-font-size', $btn-font-size); + @include print-variable('btn-padding-y-sm', $btn-padding-y-sm); + @include print-variable('btn-padding-x-sm', $btn-padding-x-sm); + @include print-variable('btn-font-size-sm', $btn-font-size-sm); + @include print-variable('btn-padding-y-lg', $btn-padding-y-lg); + @include print-variable('btn-padding-x-lg', $btn-padding-x-lg); + @include print-variable('btn-font-size-lg', $btn-font-size-lg); + @include print-variable('btn-border-width', $btn-border-width); + @include print-variable('btn-border-radius', $btn-border-radius); + @include print-variable('btn-border-radius-sm', $btn-border-radius-sm); + @include print-variable('btn-border-radius-lg', $btn-border-radius-lg); + + @include print-variable('input-padding-y', $input-padding-y); + @include print-variable('input-padding-x', $input-padding-x); + @include print-variable('input-font-size', $input-font-size); + @include print-variable('input-padding-y-sm', $input-padding-y-sm); + @include print-variable('input-padding-x-sm', $input-padding-x-sm); + @include print-variable('input-font-size-sm', $input-font-size-sm); + @include print-variable('input-padding-y-lg', $input-padding-y-lg); + @include print-variable('input-padding-x-lg', $input-padding-x-lg); + @include print-variable('input-font-size-lg', $input-font-size-lg); + @include print-variable('input-border-width', $input-border-width); + @include print-variable('input-border-radius', $input-border-radius); + @include print-variable('input-border-radius-sm', $input-border-radius-sm); + @include print-variable('input-border-radius-lg', $input-border-radius-lg); + + @include print-variable('number-of-color-palettes', length($o-color-palettes) - 1); // -1 since the last one is the user customized one + @include print-variable('color-palettes-number', $o-original-color-palette-number); + @include print-variable('has-customized-colors', $o-has-customized-colors); + + // 6) Get list of colorpalette custom colors + $custom-colors: (); + @each $key, $value in $o-color-palette { + $custom-colors: append($custom-colors, $key); + } + @include print-variable('custom-colors', $custom-colors); +} + +#wrapwrap { + @if o-website-value('body-image') { + background-image: url("/#{str-slice(o-website-value('body-image'), 2)}"); + background-position: center; + background-attachment: fixed; + @if o-website-value('body-image-type') == 'pattern' { + background-size: auto; + background-repeat: repeat; + } @else { + background-size: cover; + background-repeat: no-repeat; + } + } + + @if o-website-value('layout') != 'full' { + > main { + background-color: o-color('o-cc1-bg'); + } + + @include media-breakpoint-up(sm) { + padding-right: $grid-gutter-width * 2; + padding-left: $grid-gutter-width * 2; + + > * { + // When the website is visually acting like a container (eg. + // boxed layout), increase its maximum size to handle bigger + // horizontal paddings. + $-max-widths: (); + @each $key, $value in $container-max-widths { + $-max-widths: map-merge($-max-widths, ( + #{$key}: $value + $grid-gutter-width * 2, + )); + } + @include make-container(0); + @include make-container-max-widths($-max-widths); + } + + > header .container { + max-width: 100% !important; + } + + // Vertical alignment when top-menu has visually "no background" + @if o-color('menu') == o-color('body') { + > header { + .navbar, .container { + padding-left: 0; + padding-right: 0; + } + } + } + } + + @if o-website-value('layout') == 'framed' { + @include media-breakpoint-up(md) { + padding-top: $grid-gutter-width; + padding-bottom: $grid-gutter-width * 1.5; + } + } @else if o-website-value('layout') == 'postcard' { + @include media-breakpoint-up(md) { + $-border-radius: $border-radius-lg; + // Don't know why (browser rounding mistake?) but the inner + // border radius must be 1px lower for this to be visually ok + // (despite the fact there is no border or any space) + $-inner-border-radius: $-border-radius - 0.0625rem; + > * { + margin-bottom: $spacer * 2; + } + > header { + &, &.o_header_affix { + .navbar { + @include border-bottom-radius($-border-radius); + } + } + } + > main, > footer { + @include border-radius($-border-radius); + + .oe_structure > :first-child { + @include border-top-radius($-inner-border-radius); + } + } + > main .oe_structure > :last-child, + .o_footer_copyright { + @include border-bottom-radius($-inner-border-radius); + } + } + } + } +} + +.navbar { + + .navbar-collapse { + min-width: 0; // Allows it to shrink during loading + } + .nav-item { + transition: opacity 1000ms ease 0s; + } + .btn { + // This was a default bootstrap style before but it was removed from + // the library at some point. It seems important in the header so that + // the header does not flicker during loading. + white-space: nowrap; + } + .o_menu_loading { + flex-wrap: nowrap !important; + overflow: hidden !important; + + .nav-item { + opacity: 0 !important; + } + } +} +.navbar-brand, .navbar-text, .navbar .nav-link { + @if $o-theme-navbar-font != $o-theme-font { + font-family: $o-theme-navbar-font; + } +} + +.navbar-light { + // Style only navbar-light which Odoo is only supposed to use in standard + // anyway. Automatically mimic navbar-dark if the user's menu color is dark. + // Note: this only works because navbar-light is defined before navbar-dark, + // we may want to use a safest way when possible. + @include o-apply-colors('menu'); + @if (color-yiq(o-color('menu')) != $yiq-text-dark) { + @extend .navbar-dark; + } +} + +$-header-nav-link-height: $nav-link-height; +@if o-website-value('header-font-size') { + $-header-nav-link-height: o-website-value('header-font-size') * $line-height-base + $nav-link-padding-y * 2; + header { + font-size: o-website-value('header-font-size'); + + .dropdown-menu, .btn { + font-size: inherit; + } + } +} +@if $o-theme-navbar-logo-height { + // With default values, this makes it slightly bigger than standard + // navbar-brand, which is what we want + header .navbar-brand { + font-size: $o-theme-navbar-logo-height / $line-height-base; + + $-logo-padding-y: max(0, $-header-nav-link-height - $o-theme-navbar-logo-height) / 2; + &, &.logo { + padding-top: $-logo-padding-y; + padding-bottom: $-logo-padding-y; + } + } +} + +.o_footer { + @include o-apply-colors('footer'); + + .o_footer_copyright { + @include o-apply-colors('copyright', $background: o-color('footer')); + } +} + +h2, h3, h4, h5, h6 { + color: color('o-cc1-h2'); +} +h3, h4, h5, h6 { + color: color('o-cc1-h3'); +} +h4, h5, h6 { + color: color('o-cc1-h4'); +} +h5, h6 { + color: color('o-cc1-h5'); +} +h6 { + color: color('o-cc1-h6'); +} +.btn { + @if ($o-theme-buttons-font != $o-theme-font) { + font-family: $o-theme-buttons-font; + } +} + +// Texts +font[style*='background'], +font[class*='bg-'] { + padding: 2px 6px 4px; +} + +// Icons +.fa { + font-family: "FontAwesome" !important; + $size: 3rem; + + &.rounded-circle, + &.rounded, + &.rounded-0, + &.rounded-leaf, + &.img-thumbnail, + &.shadow { + display: inline-block; + vertical-align: middle; + text-align: center; + // fa-1x is not ouput + @include size($size); + line-height: $size; + @for $i from 2 through 5 { + &.fa-#{$i}x { + @include size($size + $i); + line-height: $size + $i; + } + } + // Default, if no background-color already selected + background-color: $gray-100; + } + &.img-thumbnail { + padding: 0; + } + &.rounded-leaf { + border-top-left-radius: $size; + border-bottom-right-radius: $size; + } + &.rounded-empty-circle { + @extend .rounded-circle; + border-width: ceil(1.4 * $border-width); + border-style: solid; + background: transparent; + } +} +// Smaller container +.o_container_small { + @extend .container; + @include media-breakpoint-up(lg) { + max-width: map-get($container-max-widths, md); + } +} + +// Buttons +.btn { + &.flat { + border: 0; + letter-spacing: 0.05em; + text-transform: uppercase; + @include button-size(0.75rem, 1.5rem, ($font-size-base * .75), $btn-line-height, 0); + &.btn-lg { @include button-size(1rem, 2rem, ($font-size-lg * .75), $btn-line-height-lg, 0); } + &.btn-sm { @include button-size(.5rem, 1rem, ($font-size-sm * .75), $btn-line-height-sm, 0); } + &.btn-xs { @include button-size(.25rem, .5rem, ($font-size-base * .5), $btn-line-height-sm, 0); } + } + &.rounded-circle { + border-radius: 100px !important; + @include button-size(0.45rem, 1.35rem, $font-size-base, $btn-line-height, 30px); + &.btn-lg { @include button-size(.6rem, 1.8rem, $font-size-lg, $btn-line-height-lg, 30px); } + &.btn-sm { @include button-size(.3rem, .9rem, $font-size-sm, $btn-line-height-sm, 30px); } + &.btn-xs { @include button-size(.15rem, .45rem, ($font-size-base * .75), $btn-line-height-sm, 30px); } + } +} + +// Background Images +.oe_img_bg { + background-size: cover; + background-repeat: no-repeat; + + &.o_bg_img_opt_repeat { + background-size: auto; + background-repeat: repeat; + } + + // Compatibility <= 13.0, TODO remove? + // ----------------------------------- + &.o_bg_img_opt_contain { + background-size: contain; + background-position: center center; + } + &.o_bg_img_opt_custom { + background-size: auto; + } + &.o_bg_img_opt_repeat_x { + background-repeat: repeat-x; + } + &.o_bg_img_opt_repeat_y { + background-repeat: repeat-y; + } +} + +// Background videos +.o_bg_video_container { + @extend %o-we-background-layer; +} +.o_bg_video_iframe { + position: relative; + pointer-events: none !important; +} +.o_bg_video_loading { + @include o-position-absolute(0, 0 ,0 ,0); +} +.o_background_video, .parallax { + @extend %o-we-background-layer-parent; +} + +// Probably outdated +// Disable fixed height +@media (max-width: 400px) { + section, + .parallax, + .row, + .hr, + .blockquote { + height: auto !important; + } +} + +// Probably outdated +// Table +.table_desc { + margin: 0 0 20px 0; + width: 100%; + word-break: break-all; + border: 1px solid #dddddd; +} +.table_heading { + background-color: #f5f5f5; + border: 1px solid #dddddd; + color: #666666; + font-size: 14px; + padding: 4px; +} +table.table_desc tr td { + text-align: left; + padding: 5px; + font-size: 13px; + &:first-child { + width: 25%; + font-weight: bold; + border-bottom: 1px solid #c9c9c9; + border-right: 1px solid #c9c9c9; + border-left: none; + } + &:last-child { + border-bottom: 1px solid #c9c9c9; + } +} + +// Jumbotron +.jumbotron { + border-radius: 0; +} + +.o_full_screen_height { + display: flex; + flex-direction: column; + justify-content: space-around; + min-height: 100vh !important; +} +.o_half_screen_height { + @extend .o_full_screen_height; + min-height: 55vh !important; +} + +// TODO remove cover_full and cover_mid classes (kept for compatibility for now) +.cover_full { + @extend .o_full_screen_height; +} +.cover_mid { + @extend .o_half_screen_height; +} + +// Allows custom border radius without contents overflowing. +.card { + overflow: hidden; +} + +// +// Snippets +// + +// Carousel -> TODO: should be versioned in 000.scss file but how ? +.s_carousel, +.s_quotes_carousel { + + // Controls + .carousel-control-prev, + .carousel-control-next { + position: absolute; + cursor: pointer; + width: 8%; + opacity: 1; + } + @include media-breakpoint-down(sm) { + .carousel-control-prev, + .carousel-control-next { + display: none; // remove arrows on mobile + } + } + .carousel-control-prev { justify-content: flex-start; } + .carousel-control-next { justify-content: flex-end; } + .carousel-control-prev-icon, + .carousel-control-next-icon { + @include size(auto); + background-image: none; + color: $body-color; + &:before { + font-family: "FontAwesome"; + display: inline-block; + background-color: #fff; + } + } + // Content + .carousel-inner { + overflow: hidden; + height: 100%; + .carousel-item { + height: 100%; + } + } + // Indicators + .carousel-indicators { + position: absolute; + + li:hover:not(.active) { + background-color: rgba(255,255,255,.8); + } + } + + // Default + &.s_carousel_default { + // Controls - chevron + .carousel-control-prev-icon:before { content: "\f053" #{"/*rtl:'\f054'*/"}; margin-left: 1.5rem; } + .carousel-control-next-icon:before { content: "\f054" #{"/*rtl:'\f053'*/"}; margin-right: 1.5rem; } + .carousel-control-prev-icon:before, + .carousel-control-next-icon:before { + background-color: rgba(0,0,0,0); + font-size: 2rem; + color: #fff; + text-shadow: $box-shadow-sm; + } + // Indicators + .carousel-indicators li { + height: .6rem; + margin-bottom: .5rem; + border: 0; + border-radius: $border-radius-sm; + box-shadow: $box-shadow-sm; + } + } + + // Border + &.s_carousel_bordered { + border: 2rem solid rgba(0,0,0,0); + @include media-breakpoint-down(sm) { + border: 0.5rem solid rgba(0,0,0,0); + } + // Controls - caret + .carousel-control-prev-icon:before { content: "\f0d9"; } + .carousel-control-next-icon:before { content: "\f0da"; } + .carousel-control-prev-icon:before, + .carousel-control-next-icon:before { + @include size(2rem, 6rem); + line-height: 6rem; + font-size: 1.5rem; + } + // Indicators + .carousel-indicators li { + @include size(3rem, 1rem); + } + } + + // Circle + &.s_carousel_rounded { + // Container + // .carousel-inner { + // border-top-left-radius: 10rem; + // border-bottom-right-radius: 10rem; + // } + // Controls - arrow + .carousel-control-prev { margin-left: 1.5rem; } + .carousel-control-next { margin-right: 1.5rem; } + .carousel-control-prev-icon:before { content: "\f060"; } + .carousel-control-next-icon:before { content: "\f061"; } + .carousel-control-prev-icon:before, + .carousel-control-next-icon:before { + @include size(4rem); + line-height: 4rem; + border-radius: 50%; + font-size: 1.25rem; + } + // Indicators + .carousel-indicators li { + @include size(1rem); + border-radius: 50%; + } + } + + // Boxed + &.s_carousel_boxed { + @include make-container(); + @include make-container-max-widths(); + .carousel-item { + padding: 0 1rem; + } + // Controls - angle + .carousel-control-prev, + .carousel-control-next { + align-items: flex-end; + margin-bottom: 1.25rem; + } + .carousel-control-prev { margin-left: 3rem; } + .carousel-control-next { margin-right: 3rem; } + .carousel-control-prev-icon:before { content: "\f104"; } + .carousel-control-next-icon:before { content: "\f105"; } + .carousel-control-prev-icon:before, + .carousel-control-next-icon:before { + @include size(2rem); + line-height: 2rem; + font-size: 1.25rem; + } + // Indicators + .carousel-indicators li { + @include size(1rem); + &:hover:not(.active) { + background-color: rgba(255,255,255,.8); + } + } + } +} + + +.carousel .container { + .carousel-img img { + max-height: 95%; + padding: 10px; + } + > .carousel-caption { + @include o-position-absolute($right: 50%, $left: 50%); + bottom: 20px; + > div { + position: absolute; + text-align: left; + padding: 20px; + background: rgba(0, 0, 0, 0.4); + bottom: 20px; + } + } + > .carousel-image { + @include o-position-absolute($top: 5%, $bottom: 5%); + max-height: 90%; + margin: 0 auto; + } + .carousel-item.text_image .container { + > .carousel-caption { + left: 10%; + > div { + right: 50%; + margin-right: -20%; + max-width: 550px; + } + } + > .carousel-image { + right: 10%; + left: 50%; + } + } + .carousel-item.image_text .container { + > .carousel-caption { + right: 10%; + > div { + left: 50%; + margin-left: -20%; + max-width: 550px; + } + } + > .carousel-image { + right: 50%; + left: 10%; + } + } + .carousel-item.text_only .container { + > .carousel-caption { + left: 10%; + right: 10%; + top: 10%; + bottom: auto; + > div { + text-align: center; + background: transparent; + bottom: auto; + width: 100%; + } + } + > .carousel-image { + display: none !important; + } + } +} + +// Parallax +.parallax { + // TODO this introduces a limitation: no dropdown will be able to + // overflow. Maybe there is a better way to find. + &:not(.s_parallax_no_overflow_hidden) { + overflow: hidden; + } + + > .s_parallax_bg { + @extend %o-we-background-layer; + } + @include media-breakpoint-up(xl) { + // Fixed backgrounds are disabled when using a mobile/tablet device, + // which is not a big deal but, on some of them (iOS...), defining the + // background as fixed breaks the background-size/position props. + // So we enable this only for >= XL devices. + &.s_parallax_is_fixed > .s_parallax_bg { + background-attachment: fixed; + } + } +} +// Keeps parallax snippet element selectable when Height = auto. +.s_parallax { + min-height: 10px; +} + +// +// Layout +// + +$-transition-duration: 200ms; + +// Affixed Header +.o_header_affixed { + display: block; + @include o-position-absolute(0, 0, auto, 0); + position: fixed; + background: $light; + + &:not(.o_header_no_transition) { + transition: transform $-transition-duration; + } + + @if o-website-value('header-template') == 'boxed' { + background: transparent; + } + + &.o_header_is_scrolled { + .navbar-brand { + font-size: $o-theme-navbar-fixed-logo-height / $line-height-base; + + img { + height: $o-theme-navbar-fixed-logo-height; + } + } + @if o-website-value('header-template') == 'vertical' { + .o_header_centered_logo { + display: none; + } + @include media-breakpoint-up(lg) { + .navbar-brand { + font-size: 0; + opacity: 0; + + img { + height: 0; + } + } + } + } + } + &.o_header_standard.o_header_is_scrolled { + @if index(('menu_logo_below', 'logo_menu_below'), o-website-value('header-template')) != null { + .navbar-brand { + &, img { + transition: none; + } + } + } + } +} +#oe_main_menu_navbar + #wrapwrap .o_header_affixed { + top: $o-navbar-height; +} + +// Navbar +.navbar .o_extra_menu_items.show > ul { + > li { + + li { + border-top: 1px solid gray('200'); + } + > a.dropdown-toggle { + background-color: gray('200'); + color: inherit; // Useful when the toggle is active + pointer-events: none; // hack to prevent clicking on it because dropdown always opened + } + > ul, > .o_mega_menu { // remove dropdown-menu default style as it is nested in another one + position: static; + float: none; + display: block; + max-height: none; + margin-top: 0; + padding: 0; + border: none; + box-shadow: none; + } + > .o_mega_menu .row > div { // remove mega menu col-lg-* style + max-width: none; + flex: auto; + } + } +} + +$-off-canvas-hamburger: o-website-value('hamburger-type') == 'off-canvas'; +$-hamburger-left: o-website-value('hamburger-position') == 'left'; +$-hamburger-center: o-website-value('hamburger-position') == 'center'; +$-hamburger-right: o-website-value('hamburger-position') == 'right'; + +$zindex-website-header: $zindex-fixed !default; + +header { + &#top { + // We need this z-index for the shadow option of the header but also + // to create a stacking context so that header dropdowns appear below + // and above the same elements as the header. + z-index: $zindex-website-header; + } + &:not(.o_header_no_transition) { + #top_menu_container { + transition: all $-transition-duration; + } + .navbar-brand { + transition: margin $-transition-duration, font-size $-transition-duration, opacity $-transition-duration ease-out; + + img { + transition: height $-transition-duration; + } + } + } + + // Dropdown menus + + // In mobile there is no need to limit the height... + @include media-breakpoint-up(lg) { + .navbar .dropdown-menu { + max-height: 60vh; + overflow-y: auto; + overflow-x: hidden; // Needed because of container in container having 0px padding... TODO improve + } + } + // ... but we limit the navbar-collapse height + .navbar-collapse.show { + max-height: 80vh; + overflow-y: auto; + overflow-x: hidden; // Needed because of container in container having 0px padding... TODO improve + } + + &:not(.o_header_is_scrolled) { + $-is-hamburger: o-website-value('header-template') == 'hamburger'; + @include media-breakpoint-up(md) { + @if $-is-hamburger { + #top_menu_container { + padding-top: $spacer * 0.5; + padding-bottom: $spacer * 0.5; + } + } + } + } + + #top_menu_container { + flex-direction: inherit; + } + @if $-hamburger-center { + .collapsing, .show { + #top_menu { + @if not $-off-canvas-hamburger { + padding-top: 15vh; + padding-bottom: 15vh; + } + text-align: center; + } + } + } + + @include media-breakpoint-up(md) { + // Allow to center the logo, ignoring the toggler space + .o_navbar_toggler_container { + flex: 0 0 0; + min-width: 0; + direction: if($-hamburger-left, ltr, rtl); + } + } + + nav.navbar { + @if o-website-value('menu-border-width') { + border: o-website-value('menu-border-width') o-website-value('menu-border-style') o-color('menu-border-color') !important; + } + border-radius: o-website-value('menu-border-radius') !important; + box-shadow: o-website-value('menu-box-shadow') !important; + } +} + +@if $-off-canvas-hamburger { + #top_menu_collapse { + + &.collapsing, &.show { + // Note: position relatively to the header instead of the viewport + // because fixed position cannot work inside an element whose CSS + // transform is different to none, which the header element is + // because of header effects. + @include o-position-absolute(0, 0, 0, 0); + z-index: $zindex-sticky; + height: 100vh; + max-height: 100vh; + transition: none; + transform: none; + + &, & > .o_header_collapsed_content_wrapper { + // TODO improve: ugly code to reset a potential wrapper + display: flex !important; + flex-flow: if($-hamburger-left, row, row-reverse) nowrap !important; + align-items: stretch !important; + justify-content: flex-start !important; + } + > .o_header_collapsed_content_wrapper { + // TODO improve: ugly code to reset a potential wrapper + max-width: none !important; + padding: 0 !important; + margin: 0 !important; + } + + .o_offcanvas_menu_backdrop { + @include o-position-absolute(0, 0, 0, 0); + opacity: .2; + cursor: pointer; + } + + #top_menu { + flex: 0 0 auto !important; + overflow: auto; + flex-flow: column nowrap !important; + @if $-hamburger-center { + width: 100%; + max-width: none; + } @else { + max-width: 560px; + text-align: left !important; + } + min-width: 250px; + margin: 0 !important; + background-color: o-color('menu'); + transition: transform $-transition-duration cubic-bezier(.694, .0482, .335, 1); + + @if $-hamburger-center { + .o_offcanvas_menu_backdrop { + display: none; + } + .o_offcanvas_menu_toggler { + max-width: 90%; + } + } + + .nav-item, .o_offcanvas_logo { + padding-left: $grid-gutter-width; + padding-right: $grid-gutter-width; + } + .nav-item, .dropdown-menu { + text-align: inherit; + } + .nav-item, .nav-link { + margin: 0 !important; + } + + .navbar-toggler { + display: block !important; + } + + // Open all dropdowns + .dropdown-toggle { + padding-bottom: $nav-link-padding-y*0.5; + + &:after { + content: none; + } + } + .dropdown-menu { + display: block; + padding-top:0; + border: 0; + background: inherit; + color: inherit; + } + .dropdown-item { + padding-left: .5em; + padding-right: .5em; + } + } + + .o_connected_user:not(.editor_has_snippets) header & { + top: -$o-navbar-height; + padding-top: $o-navbar-height; + } + } + &.collapsing #top_menu { + @if $-hamburger-center { + transform: translateY(-100%); + } @else if $-hamburger-left { + transform: translateX(-100%); + } @else if $-hamburger-right { + transform: translateX(100%); + } + } + &.show #top_menu { + transform: translate(0); + } + + .o_offcanvas_menu_backdrop { + @if $-hamburger-left { + background-image: linear-gradient(90deg, currentColor 20%, transparent); + } @else { + background-image: linear-gradient(-90deg, currentColor 20%, transparent); + } + } + } +} + +@if o-website-value('header-template') == 'vertical' { + header .o_header_centered_logo { + order: -1; + width: 50%; + margin-top: $spacer; + + @include media-breakpoint-up(lg) { + order: inherit; + width: 40%; + margin-bottom: $spacer; + } + } + .navbar-nav { + padding-top: $navbar-padding-y; + padding-bottom: $navbar-padding-y; + } +} @else if o-website-value('header-template') == 'sidebar' { + @include media-breakpoint-up(lg) { + #wrapwrap { + // Hack: padding is used by layout option (boxed, etc) so use + // border here to be able to combine the effect. + @if $-hamburger-right { + border-right: o-website-value('sidebar-width') solid transparent; + } @else { + border-left: o-website-value('sidebar-width') solid transparent; + } + + > header { + @if $-hamburger-right { + @include o-position-absolute(0, 0, 0, auto); + } @else { + @include o-position-absolute(0, auto, 0, 0); + } + position: fixed; + z-index: $zindex-fixed; + display: flex; + width: o-website-value('sidebar-width'); + transform: none !important; + + .navbar { + width: 100%; + align-items: start; + padding: $spacer; + + .navbar-brand { + max-width: 100%; + padding: 0 0 $spacer 0; + } + #top_menu_container { + flex-direction: column; + align-items: start; + padding: 0; + } + .navbar-nav { + flex-direction: column; + } + .nav-link, + .dropdown-item { + white-space: initial; + } + .dropdown-menu { + position: static; + } + } + } + } + body.o_connected_user { + &:not(.editor_has_snippets) #wrapwrap > header { + top: $o-navbar-height; + } + &.editor_has_snippets #wrapwrap > header { + @if $-hamburger-right { + right: $o-we-sidebar-width; + } + } + } + } +} @else if o-website-value('header-template') == 'boxed' { + #wrapwrap:not(.o_header_overlay) .o_header_boxed_background { + @include o-apply-colors('header-boxed'); + } +} @else if o-website-value('header-template') == 'centered_logo' { + header .o_header_centered_logo { + @include media-breakpoint-up(lg) { + width: 50%; + } + } +} @else if o-website-value('header-template') == 'hamburger-full' { + @if not $-off-canvas-hamburger { + @include media-breakpoint-up(md) { + #wrapwrap { + $o-hamburger-full-navbar-height: $o-theme-navbar-logo-height + ($navbar-padding-y * 2); + > header { + .navbar-collapse { + > .container { + height: calc(100vh - #{$o-navbar-height} - #{$o-hamburger-full-navbar-height}); + transition: height .3s ease; + } + .nav-link { + padding-right: $nav-link-padding-x; + padding-left: $nav-link-padding-x; + } + .dropdown-menu { + position: absolute; + } + } + } + } + } + } +} + +// Mega menu +.o_mega_menu { + width: 100%; + padding: 0; + margin-top: 0; + border-radius: 0; + background-clip: unset; // Remove the 1px gap introduced by BS4 + + .container, .container-fluid { + // Need to reforce those because they are removed since its a container + // inside another container (the one in the navbar) + padding-left: $grid-gutter-width / 2; + padding-right: $grid-gutter-width / 2; + } +} +.o_mega_menu_container_size { + @include media-breakpoint-up(md) { + left: 50%; + transform: translateX(-50%); + } + + $-mm-max-widths: (); + @each $k, $v in $container-max-widths { + $-mm-max-widths: map-merge($-mm-max-widths, ( + #{$k}: $v - $grid-gutter-width, + )); + } + @include make-container-max-widths($-mm-max-widths); +} + +#wrapwrap.o_header_overlay { + > header:not(.o_header_affixed):not(.o_top_menu_collapse_shown) { + @include o-position-absolute(0, 0, auto, 0); + z-index: 1000; + + > .navbar { + @include o-apply-colors(1); // Reset to default colored components + background-color: transparent !important; + border-color: transparent; + color: inherit; + + .nav-item { + > .nav-link { + &, &:hover { + background-color: transparent; + color: inherit; + } + + &.active { + font-weight: bolder; + } + } + } + } + } +} + +// Navbar Links Styles +@if index(('block', 'border-bottom'), o-website-value('header-links-style')) { + @include media-breakpoint-up(md) { + .navbar, + .navbar-nav { + padding-top: 0; + padding-bottom: 0; + } + } +} +.navbar-nav { + .nav-link { + @if o-website-value('header-links-style') == 'outline' { + // Need to force the padding in this case so that it stays in mobile + padding-right: $navbar-nav-link-padding-x; + padding-left: $navbar-nav-link-padding-x; + border: $border-width solid transparent; + @include border-radius($nav-pills-border-radius); + } @else if o-website-value('header-links-style') == 'block' { + // There is no way to control navbar links vertical padding in BS4 + // independently from nav ones, just double them here instead + padding-top: $nav-link-padding-y * 2; + padding-bottom: $nav-link-padding-y * 2; + @include border-radius(0); + } @else if o-website-value('header-links-style') == 'border-bottom' { + // There is no way to control navbar links vertical padding in BS4 + // independently from nav ones, just double them here instead + padding-top: ($nav-link-padding-y * 2); + padding-bottom: ($nav-link-padding-y * 2); + border-bottom: $nav-link-padding-y solid transparent; + + // Replace horizontal paddings by margins (do this with an extra + // class to override .navbar-expand-* paddings priority). + .navbar & { + padding-left: 0; + padding-right: 0; + margin: 0 $navbar-nav-link-padding-x; + } + } + } + + @if index(('outline', 'border-bottom'), o-website-value('header-links-style')) { + .nav-link.active, + .show > .nav-link { + border-color: currentColor; + } + } +} + +@if index(('slideout_slide_hover', 'slideout_shadow'), o-website-value('footer-effect')) { + @include media-breakpoint-up(lg) { + #wrapwrap.o_footer_effect_enable { + > main { + background-color: $body-bg; + @if o-website-value('footer-effect') == 'slideout_shadow' { + box-shadow: $box-shadow; + } + } + > footer { + @include o-position-sticky(auto, 0, 0, 0); + z-index: -1; + } + } + } +} + +// Language selector +.js_language_selector { + .dropdown-menu { + min-width: 0; + } + a.list-inline-item { + padding: 3px 0; + + > * { + vertical-align: middle; + } + } +} +.o_lang_flag { + width: 1.5em; + height: 1.5em; + margin-right: 0.2em; + border-radius: $rounded-pill; +} +span.list-inline-item.o_add_language:last-child { + display: none !important; // Hide the separator if it is the last list item +} + +// Footer scrolltop button +@if o-website-value('footer-scrolltop') { + #o_footer_scrolltop { + $-footer-color: o-color('footer'); + $-copyright-color: o-color('copyright'); + $-copyright-color: mix(rgba($-copyright-color, 1.0), $-footer-color, percentage(alpha($-copyright-color))); + + box-sizing: content-box; + width: 3rem; + height: 3rem; + border: 0; + padding: 0; + @include o-apply-colors('footer', $with-extras: false, $background: $-footer-color); + text-decoration: none; + + @if $-footer-color == $-copyright-color { + color: rgba(color-yiq($-footer-color), 0.5); + } + + @include hover-focus { + @include o-apply-colors($-copyright-color, $with-extras: false, $background: $-footer-color); + text-decoration: none; + } + } +} + +// Figure with special style +.o_figure_relative_layout { + position: relative; + + .figure-img { + margin-bottom: 0; + } + .figure-caption { + @include o-position-absolute(auto, 0, 0, 0); + @include o-bg-color(rgba(theme-color('dark'), $o-theme-figcaption-opacity)); + padding: $tooltip-padding-y $tooltip-padding-x; + font-weight: $font-weight-bold; + a { + color: inherit; + } + } +} + +@each $color, $value in $theme-colors { + .bg-#{$color}-light { + background-color: rgba($value, 0.1); + } +} + +@each $media, $color in $o-social-colors { + @include text-emphasis-variant(".text-#{$media}", $color); +} + +// TODO: Will be handled properly in master/saas-12.2, temp fix for website_event.registration_attendee_details +.modal-footer > .float-left { + margin-right: auto; +} + +// CoverProperties +.o_record_cover_container { + position: relative; + + .o_record_cover_component { + @include o-position-absolute(0, 0, 0, 0); + + background-size: cover; + background-position: center; + background-repeat: no-repeat; + } +} + +// Scroll down button +.o_scroll_button { + @include o-position-absolute(auto, 0, 0, 0); + display: flex; + width: 50px; + height: 50px; + animation: o-anim-heartbeat 2.6s ease-in-out 1s infinite; + + &, &:hover { + text-decoration: none; + } + &:focus { + outline: none; + } + &:hover { + animation-iteration-count: 1; + } +} + +// Attention keeper for the "scroll down" top-banner button +@keyframes o-anim-heartbeat { + 0%, 14%, 35% { + transform: scale(1); + } + 7%, 21% { + transform: scale(1.3); + background-color: rgba(theme-color('primary'), 0.8); + } +} + +// Ribbons +$ribbon-padding: 100px; +.o_ribbon { + margin: 0; + font-size: 1rem; + font-weight: bold; + white-space: nowrap; + text-align: center; + pointer-events: none; +} + +.o_ribbon_right { + @include o-ribbon-right(); +} + +.o_ribbon_left { + @include o-ribbon-left(); +} + +.o_tag_right { + @include o-tag-right(); +} + +.o_tag_left { + @include o-tag-left(); +} + +// Cookies Bar +#website_cookies_bar { + :not(.o_cookies_popup) { + bottom: 0; + } +} + +.o_website_btn_loading { + cursor: wait; + opacity: $btn-disabled-opacity; + .fa:not(.fa-spin) { + display: none; + } +} + +// Snippet Showcase +.s_showcase_icon { + // Avoid images stretched depending on title size (when icons + // are images an not Font Awesome icons). Because the default + // value of "align-self" is "strech". We put this code here to + // avoid having to create a new scss file in a stable version. + align-self: flex-start; +} + +// Bottom fixed element (e.g. livechat button) +.modal-open .o_bottom_fixed_element, .o_bottom_fixed_element_hidden { + // Prevent bottom fixed elements from hidding buttons and + // hide them if a modal is open. + display: none !important; +} diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss new file mode 100644 index 00000000..da23fabc --- /dev/null +++ b/addons/website/static/src/scss/website.ui.scss @@ -0,0 +1,513 @@ +/// +/// This file regroups main website UI layout rules (when the user is connected) +/// and the UI components rules. +/// + +// LAYOUTING +body { + // Set frontend direction that will be flipped with + // rtlcss for right-to-left text direction. + direction: ltr; +} +body.o_connected_user { + padding-top: $o-navbar-height!important; + + &.o_fullscreen_transition { + transition: padding 400ms ease 0s; + + #oe_main_menu_navbar, #web_editor-top-edit, .o_we_website_top_actions, #oe_snippets { + transition: transform 400ms ease 0s !important; + } + .o_header_affixed { + transition: top 0.35s !important; + } + } + &.o_fullscreen { + padding-top: 0 !important; + + &.editor_enable.editor_has_snippets { + padding-right: 0 !important; + } + #oe_main_menu_navbar, #web_editor-top-edit { + transform: translateY(-100%); + } + .o_we_website_top_actions, #oe_snippets { + transform: translateX(100%); + } + .o_header_affixed { + top: 0 !important; + right: 0 !important; + } + } +} + +// MAIN MENU STYLE (added above navbar.scss) +#oe_main_menu_navbar { + @include o-w-preserve-dropdown-menus; + @include o-position-absolute(0, 0, auto, 0); + position: fixed; + z-index: $zindex-modal - 10; + font-family: $o-we-font-family; + font-size: 14px; + + a:hover, a:focus { + text-decoration: none; + } + .dropdown-menu { + font-size: inherit; + border-radius: 0; + color: $dropdown-link-active-color; + } + + .o_menu_sections { + .o_mobile_preview a { + text-align: center; + font-size: 20px; + } + } + .o_menu_systray { + > li > a { + padding: 0 $grid-gutter-width/2; + + &.css_edit_dynamic{ + padding: 0 $grid-gutter-width/4; + } + + &[data-action="edit"], &[data-action="translate"], &.css_edit_dynamic { + @include button-variant($o-brand-primary, $o-brand-primary); + } + + &, &:hover, &:focus { + text-decoration: none; + } + } + + .o_mobile_preview a { + text-align: center; + font-size: 20px; + } + } + @include media-breakpoint-down(sm) { + #oe_applications { + position: inherit; + z-index: 1002; + } + } +} + +@mixin o-w-close-icon($size:12px, $color:#000, $color-hover:#000, $thickness: 1px, $opacity: 0.7, $opacity-hover: 1) { + color: transparent; + position: relative; + display: inline-block; + opacity: $opacity; + width: $size; + height: $size; + + &:hover, &:focus { + outline: none; + opacity: $opacity-hover; + + &::after, &::before { + background: $color-hover; + } + } + + &:after, &:before { + content: ''; + margin-top: -1px; + background: $color; + @include size(100%, $thickness); + @include o-position-absolute(50%, $left:0); + transform: rotate(45deg); + } + + &:after { + transform: rotate(-45deg); + } +} + +// BLOCKING LOADER +#o_website_page_loader { + @include o-position-absolute(0, 0, 0, 0); + z-index: $zindex-modal - 1; + background-color: rgba(0, 0, 0, 0.8); +} + +// MODALS +body .modal { + &.o_technical_modal { + @include o-w-preserve-base; + @include o-w-preserve-dropdown-menus; + @include o-w-preserve-headings; + @include o-w-preserve-forms; + @include o-w-preserve-links; + @include o-w-preserve-btn; + @include o-w-preserve-cards; + @include o-w-preserve-modals; + @include o-w-preserve-tabs; + } + + // MOBILE PREVIEW + &.oe_mobile_preview { + text-align: center; + + .modal-dialog { + display: inline-block; + width: auto; + + .modal-content { + background-color: black!important; + border: 3px outset gray; + border-radius: 20px; + + .modal-header { + border: none; + cursor: pointer; + font-family: $o-we-font-family; + + &, .close { + color: white; + } + + h4 { + font-family: inherit; + font-weight: normal; + color: inherit; + + .fa { + margin-left: $grid-gutter-width/2; + } + } + .close { + color: #4e525b; + } + } + + .modal-body { + background-color: inherit!important; + border-radius: 20px; + padding: 15px; + + $mobile-preview-width: 320px; + $mobile-preview-height: 530px; + + display: flex; + width: $mobile-preview-width + 15; + height: $mobile-preview-height; + transition: all 400ms ease 0s; + + &.o_invert_orientation { + width: $mobile-preview-height + 15; + height: $mobile-preview-width; + } + + > iframe { + display: block; + width: 100%; + border: none; + } + } + + .modal-footer { + display: none; + } + } + } + } + + // TOP MENU EDITOR + .oe_menu_editor { + ul { + padding-left: 37px; + } + + li { + margin-top: -1px; + + .input-group-addon { + border-radius: 0; + } + } + } + + // SEO CONFIGURATION + &.oe_seo_configuration { + #language-box { + padding-right: 25px; + background-color: white; + } + .o_seo_og_image { + .o_meta_img { + position: relative; + transition: border-color 200ms; + display: inline-block; + border: 2px solid gray('400'); + + > img { + width: 70px; + height: 70px; + object-fit: cover; + cursor: pointer; + } + + &:hover { + border-color: $o-brand-primary; + } + + &.o_active_image { + border-color: $o-brand-primary; + + &:before { + @include o-position-absolute($right: 0); + content: ''; + border: 16px solid rgba($o-brand-primary, 0.8); + border-left-color: transparent; + border-bottom-color: transparent; + } + + &:after { + @include o-position-absolute(2px, 3px); + display: inline-block; + content: "\f00c"; + font-family: FontAwesome; + color: white; + font-size: 12px; + } + } + .o-custom-label { + @include o-position-absolute($bottom: 0px); + background: rgba(gray('800'), 0.6); + font-size: 12px; + } + } + .o_meta_img_upload { + transition: 200ms; + display: inline-block; + padding: 23px 27px; + border: 3px dashed lighten(gray('700'), 30%); + vertical-align: top; + cursor: pointer; + color: lighten(gray('600'), 30%); + + &:hover { + border-color: $o-brand-primary; + color: $o-brand-primary; + } + } + .o_meta_active_img { + height: 240px; + object-fit: cover; + } + } + + div.oe_seo_preview_g { + list-style: none; + font-family: arial, sans-serif; + + .r { + cursor: pointer; + color:#1a0dab; + font-size: 18px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-text-overflow: ellipsis; + white-space: nowrap; + } + + .s { + font-size: 13px; + line-height: 18px; + color: #545454; + .kv { + color: #006621; + font-size: 14px; + line-height: 18px; + } + } + } + + td.o_seo_keyword_suggestion span.o_seo_suggestion.badge { + cursor: pointer; + } + } +} + +// ADD NEW PAGE MODAL + +.o_new_content_open { + // Kill the scroll on the body + overflow: hidden; +} + +#o_new_content_menu_choices { + @include o-w-preserve-base; + @include o-position-absolute($o-navbar-height, 0, 0, 0); + position: fixed; + display: flex; + overflow: auto; + background-color: rgba(0, 0, 0, 0.8); + font-family: $o-we-font-family; + + &::before { + content: " "; + @include o-position-absolute(0, 0, 0, 0); + z-index: -1; + pointer-events: none; + } + + .container { + max-width: 720px; + margin: auto; + } + + .o_new_content_element { + opacity: 0; + animation: fadeInDownSmall 1s forwards; + + .o_uninstalled_module { + filter: brightness(70%) contrast(60%); + + } + + .module_icon { + display: block; + margin: auto; + } + + a { + display: block; + font-size: 34px; + text-align: center; + text-decoration: none; + + i { + width: 110px; + height: 110px; + border: 3px solid lighten(#2C2C36, 10%); + border-radius: 100%; + line-height: 104px; + background-color: #2C2C36; + color: white; + + transition: all 0.5s cubic-bezier(0.19, 1, 0.22, 1) 0s; + } + p { + color: white; + margin-top: 0.7em; + font-size: 0.5em; + } + + &:hover, &:focus { + text-decoration: none; + outline: none; // remove ugly dotted border on Firefox + i { + border-color: #1cc1a9; + box-shadow: 0 0 10px rgba(28, 193, 169, 0.46); + } + } + } + } +} + +// LOGIN FORM +.oe_login_form, .oe_signup_form, .oe_reset_password_form { + max-width: 300px; + position: relative; + margin: 50px auto; +} + +// ACE EDITOR +.o_ace_view_editor { + @include o-w-preserve-base; + @include o-w-preserve-btn; + @include o-w-preserve-forms; + + @include o-position-absolute($o-navbar-height, 0, 0); + position: fixed; + z-index: $zindex-modal; +} + +// POPOVER NAVIGATION +.tour .popover-navigation { + margin-left: 13px; + margin-bottom: 8px; +} + +// PUBLISH +.css_published { + .btn-danger, .css_publish { + display: none; + } +} +.css_unpublished { + .btn-success, .css_unpublish { + display: none; + } +} +[data-publish='off'] > *:not(.css_options) { + opacity: 0.5; +} + +// Do not show path behind the links in browser printing +@media print { + a[href]:after { + content: initial; + } +} + +// Pages Management +.o_page_management_info { + .o_switch { + padding-top: 9px; + } +} +#list_website_pages { + th { + background-color: $o-brand-odoo; + color: white; + } + td, th { + padding: 0.45rem; + } + td { + > a.fa { + margin-left: 5px; + color: $o-brand-odoo; + } + .text-muted { + opacity: 0.5; + } + } + .fa-check, .fa-eye-slash { + color: $info; + } +} + +.ui-autocomplete.o_website_ui_autocomplete { + max-width: 400px; + font-size: $o-we-sidebar-font-size; + border: none; + background-color: $o-we-sidebar-content-field-dropdown-bg; + box-shadow: $o-we-sidebar-content-field-dropdown-shadow; + > li { + border-bottom: $o-we-sidebar-content-field-border-width solid lighten($o-we-sidebar-content-field-dropdown-border-color, 15%); + border-radius: $o-we-sidebar-content-field-border-radius; + background-color: $o-we-sidebar-content-field-clickable-bg; + color: $o-we-sidebar-content-field-clickable-color; + &.ui-menu-item { + > div { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 20px; + &.ui-state-active { + border: $o-we-sidebar-content-field-dropdown-border-width solid transparent; + background-color: $o-we-sidebar-content-field-dropdown-item-bg-hover; + } + } + } + &.ui-autocomplete-category { + background-color: $o-we-bg-lighter; + } + } +} diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss new file mode 100644 index 00000000..32c86a63 --- /dev/null +++ b/addons/website/static/src/scss/website.wysiwyg.scss @@ -0,0 +1,235 @@ + +@each $font-name, $font-config in $o-theme-font-configs { + $url: map-get($font-config, 'url'); + @if $url { + @import url("https://fonts.googleapis.com/css?family=#{unquote($url)}&display=swap"); + } +} + +:root { + // Override css variables to influence the default style of the editor + // without duplicating the css. + @include print-variable('o-we-toolbar-height', $o-navbar-height); + + // Need info about the color of each color palette + @for $index from 1 through length($o-color-palettes) - 1 { // Not the user one + @each $name, $color in nth($o-color-palettes, $index) { + @include print-variable('o-palette-#{$index}-#{$name}', $color); + } + } +} + +// EDITOR TOP BAR AND POPOVER +.o_we_website_top_actions { + @include o-position-absolute($top: 0, $right: 0); + z-index: ($zindex-fixed + $zindex-modal-backdrop) / 2 + 2; // $o-we-zindex + display: flex; + justify-content: flex-end; + width: $o-we-sidebar-width; + height: $o-we-sidebar-top-height; + background-color: $o-we-sidebar-tabs-bg; + + .btn-group, .btn { + height: 100%; + } + + .btn { + border: none; + border-radius: 0; + padding: 0.375rem 0.75rem; + font-size: $o-we-font-size; + font-family: $o-we-font-family; + font-weight: 400; + line-height: 1; + + &.btn-primary { + @include button-variant($o-brand-primary, $o-brand-primary); + } + &.btn-secondary { + @include button-variant($o-we-sidebar-tabs-bg, $o-we-sidebar-tabs-bg); + } + &:focus, &:active, &:focus:active { + outline: none; + box-shadow: none; + } + } + + .dropdown-menu { + left: auto; + right: 0; + } +} + +#oe_snippets { + top: $o-we-sidebar-top-height; +} + +.note-statusbar { + display: none; +} + +// TRANSLATIONS +.oe_translate_examples li { + margin: 10px; + padding: 4px; +} + +html[lang] > body.editor_enable [data-oe-translation-state] { + background: rgba($o-we-content-to-translate-color, 0.5) !important; + + &[data-oe-translation-state="translated"] { + background: rgba($o-we-translated-content-color, 0.5) !important; + } + + &.o_dirty { + background: rgba($o-we-translated-content-color, 0.25) !important; + } +} + +// SNIPPET PANEL +$i: 1; +@each $font-name, $font-config in $o-theme-font-configs { + we-toggler.o_we_option_font_#{$i}, we-button.o_we_option_font_#{$i} > div { + font-family: o-safe-get($font-config, 'family', $font-family-base); + + &::before { + content: $font-name; + } + } + $i: $i + 1; +} +.o_we_add_google_font_btn { + border-top: 1px solid currentColor !important; +} + +#oe_snippets > .o_we_customize_panel { + .o_we_user_value_widget.o_palette_color_preview_button { + > div { + display: flex; + flex: 1 1 auto; + align-items: stretch; + } + .o_palette_color_preview { + flex: 1 0 0; + margin: 1px 0; + transition: flex 150ms ease 0s; + } + &:not(:hover) .o_palette_color_preview { + &:nth-child(4), &:nth-child(5) { + flex: 0 0 0; + } + } + } + + we-select.o_scroll_effects_selector we-button { + padding-top: $o-we-item-spacing; + padding-bottom: $o-we-item-spacing; + + img { + max-height: 80px; + width: auto; + margin-right: $o-we-item-spacing; + margin-left: $o-we-item-spacing * .5; + } + } + + //---------------------------------------------------------------------- + // 'Options' Tab Specific Components + //---------------------------------------------------------------------- + + // Theme Colors Editor + .o_we_theme_colors_selector { + + > we-title { + display: none + } + .o_we_so_color_palette.o_we_user_value_widget { + + + .o_we_so_color_palette { + margin-left: 10px; + } + .o_we_color_preview { + width: $o-we-sidebar-content-field-colorpicker-size-large; + height: $o-we-sidebar-content-field-colorpicker-size-large; + } + } + > div, we-select.o_we_theme_colors_select, we-toggler { + display: flex; + } + > div { + align-items: stretch; + width: 100%; + } + we-select.o_we_theme_colors_select { + justify-content: flex-end; + margin-left: auto; + + > div, we-toggler { + height: 100%; + } + } + we-toggler { + align-items: center; + padding: 0 0.4rem; + font-size: 1.5em; + + &:after { + content: none; + } + } + } + + // Palettes Dropdown + .o_palette_color_preview_button > div { + min-height: 24px; + } + + // CC Edition + .o_we_cc_preview_wrapper { + // Use box-shadow rather than border-bottom in order to + // avoid misalignments in the 'Options' tab. + border: 1px solid; + border-color: rgba($o-we-item-standup-color-light, .2) $o-we-sidebar-content-field-dropdown-border-color transparent; + box-shadow: 0 1px 0 $o-we-item-standup-color-dark; + + + .o_we_collapse_toggler { + height: 35px; // FIXME hardcoded... + } + } +} + +// SNIPPET OPTIONS +.o_we_border_preview { + display: inline-block; + width: 999px; + max-width: 100%; + margin-bottom: 2px; + border-width: 4px; + border-bottom: none !important; +} + +.pac-container { // google map autosuggestion + z-index: $zindex-modal-backdrop; // > $o-we-zindex + width: $o-we-sidebar-width !important; + font-size: $o-we-sidebar-font-size; + margin-left: -$o-we-sidebar-width/2; + border-top: none; + background-color: $o-we-sidebar-content-field-dropdown-bg; + box-shadow: $o-we-sidebar-content-field-dropdown-shadow; + &:after { + display: none; + } + .pac-item { + border-top: $o-we-sidebar-content-field-border-width solid lighten($o-we-sidebar-content-field-dropdown-border-color, 15%); + border-radius: $o-we-sidebar-content-field-border-radius; + background-color: $o-we-sidebar-content-field-clickable-bg; + color: $o-we-sidebar-content-field-clickable-color; + &:hover { + background-color: $o-we-sidebar-content-field-dropdown-item-bg-hover; + cursor: pointer; + } + } + .pac-item-query { + color: $o-we-sidebar-content-field-clickable-color; + } +} diff --git a/addons/website/static/src/scss/website_visitor_views.scss b/addons/website/static/src/scss/website_visitor_views.scss new file mode 100644 index 00000000..ee209181 --- /dev/null +++ b/addons/website/static/src/scss/website_visitor_views.scss @@ -0,0 +1,46 @@ +.o_wvisitor_kanban { + &.o_kanban_ungrouped { + padding:0px; + .o_wvisitor_kanban_card { + width: 100%; + margin: 0px; + border-top: 0px !important; + &:first-child { + border-bottom: 1px solid #dee2e6 !important; + } + .o_kanban_detail_ungrouped { + .o_wvisitor_kanban_image { + width: 56px; + height: 56px; + top: 0px; + position: absolute; + } + .o_wvisitor_name { + padding-left: 72px; + } + .w_visitor_kanban_actions_ungrouped { + button { + float: right; + margin: 4px; + } + } + .o_country_flag { + margin-right: 8px; + } + } + .oe_kanban_details { + display:none !important; + } + } + } + &.o_kanban_grouped { + .o_kanban_detail_ungrouped { + display:none !important; + } + } +} + +.o_country_flag { + width:24px; + height: 16px; +}
\ No newline at end of file diff --git a/addons/website/static/src/snippets/s_alert/000.scss b/addons/website/static/src/snippets/s_alert/000.scss new file mode 100644 index 00000000..a44d1d2d --- /dev/null +++ b/addons/website/static/src/snippets/s_alert/000.scss @@ -0,0 +1,30 @@ + +.s_alert { + margin: $grid-gutter-width/2 0; + border: $alert-border-width solid; + border-radius: $alert-border-radius; + p, ul, ol { + &:last-child { + margin-bottom: 0; + } + } + &_sm { + padding: $grid-gutter-width/3; + font-size: $font-size-sm; + } + &_md { + padding: $grid-gutter-width/2; + font-size: $font-size-base; + } + &_lg { + padding: $grid-gutter-width; + font-size: $font-size-lg; + } + &_icon { + float: left; + margin-right: 10px; + } + &_content { + overflow: hidden; + } +} diff --git a/addons/website/static/src/snippets/s_badge/000.scss b/addons/website/static/src/snippets/s_badge/000.scss new file mode 100644 index 00000000..acdb811e --- /dev/null +++ b/addons/website/static/src/snippets/s_badge/000.scss @@ -0,0 +1,10 @@ + +.s_badge { + padding: $s-badge-padding; + margin: $s-badge-margin; + border-radius: if($s-badge-border-radius != null, $s-badge-border-radius, $badge-border-radius); + font-size: $font-size-sm; + .fa { + margin: $s-badge-i-margin; + } +} diff --git a/addons/website/static/src/snippets/s_badge/000_variables.scss b/addons/website/static/src/snippets/s_badge/000_variables.scss new file mode 100644 index 00000000..b6bc1929 --- /dev/null +++ b/addons/website/static/src/snippets/s_badge/000_variables.scss @@ -0,0 +1,4 @@ +$s-badge-border-radius: null; +$s-badge-padding: .5rem; +$s-badge-margin: .5rem .5rem .5rem 0; +$s-badge-i-margin: 0 .3rem 0 0; diff --git a/addons/website/static/src/snippets/s_blockquote/000.scss b/addons/website/static/src/snippets/s_blockquote/000.scss new file mode 100644 index 00000000..a41d1f27 --- /dev/null +++ b/addons/website/static/src/snippets/s_blockquote/000.scss @@ -0,0 +1,73 @@ +.s_blockquote { + // Reset + border: 0; + padding: 0; + .s_blockquote_icon { + font-size: $font-size-base; + } + .s_blockquote_author { + opacity: .75; + } + // Classic + &.s_blockquote_classic { + .s_blockquote_icon { + float: left; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + &.float-right { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + } + .s_blockquote_content { + overflow: hidden; + padding: $spacer * 1.5; + .blockquote-footer { + &::before { + content: ''; + } + .s_blockquote_avatar { + max-height: $spacer * 2.5; + } + } + } + } + // Cover + &.s_blockquote_cover { + text-align: center; + .s_blockquote_icon { + position: relative; + z-index: 1; + float: none; + margin-bottom: -$spacer * 1.5; + } + p:last-of-type { + margin-bottom: $spacer * .5; + } + .s_blockquote_content, .s_blockquote_filter { // s_blockquote_filter is there for compatibility + padding: $spacer * 3 $spacer * 2 $spacer * 2; + } + // Compatibility + .s_blockquote_filter { + margin: $spacer * -3 $spacer * -2 $spacer * -2; + } + .quote_char { + margin: $spacer * 2 0 $spacer 0; + & ~ .blockquote-footer { + padding-bottom: $spacer * 2; + } + } + } + // Minimalist + &.s_blockquote_minimalist { + border-left: 5px solid; + border-color: o-color('secondary'); + .s_blockquote_content { + padding: $spacer; + @include border-right-radius($border-radius); + p:last-of-type { + margin-bottom: 0; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_blockquote/options.js b/addons/website/static/src/snippets/s_blockquote/options.js new file mode 100644 index 00000000..46748f15 --- /dev/null +++ b/addons/website/static/src/snippets/s_blockquote/options.js @@ -0,0 +1,46 @@ +odoo.define('website.s_blockquote_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.Blockquote = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Change blockquote design. + * + * @see this.selectClass for parameters + */ + display: function (previewMode, widgetValue, params) { + + // Classic + this.$target.find('.s_blockquote_avatar').toggleClass('d-none', widgetValue !== 'classic'); + + // Cover + const $blockquote = this.$target.find('.s_blockquote_content'); + if (widgetValue === 'cover') { + $blockquote.css({"background-image": "url('/web/image/website.s_blockquote_cover_default_image')"}); + $blockquote.css({"background-position": "50% 50%"}); + $blockquote.addClass('oe_img_bg'); + if (!$blockquote.find('.o_we_bg_filter').length) { + const bgFilterEl = document.createElement('div'); + bgFilterEl.classList.add('o_we_bg_filter', 'bg-white-50'); + $blockquote.prepend(bgFilterEl); + } + } else { + $blockquote.css({"background-image": ""}); + $blockquote.css({"background-position": ""}); + $blockquote.removeClass('oe_img_bg'); + $blockquote.find('.o_we_bg_filter').remove(); + $blockquote.find('.s_blockquote_filter').contents().unwrap(); // Compatibility + } + + // Minimalist + this.$target.find('.s_blockquote_icon').toggleClass('d-none', widgetValue === 'minimalist'); + this.$target.find('footer').toggleClass('d-none', widgetValue === 'minimalist'); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_btn/000.scss b/addons/website/static/src/snippets/s_btn/000.scss new file mode 100644 index 00000000..4e5a3215 --- /dev/null +++ b/addons/website/static/src/snippets/s_btn/000.scss @@ -0,0 +1,6 @@ + +.s_btn { + .btn + .btn { + margin-left: .75rem; + } +} diff --git a/addons/website/static/src/snippets/s_card/000.scss b/addons/website/static/src/snippets/s_card/000.scss new file mode 100644 index 00000000..436d9a70 --- /dev/null +++ b/addons/website/static/src/snippets/s_card/000.scss @@ -0,0 +1,12 @@ + +.s_card { + margin: $grid-gutter-width/2 0; + .card-body { + // color: initial; + p, ul, ol { + &:last-child { + margin-bottom: 0; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_chart/000.js b/addons/website/static/src/snippets/s_chart/000.js new file mode 100644 index 00000000..2c67bd03 --- /dev/null +++ b/addons/website/static/src/snippets/s_chart/000.js @@ -0,0 +1,142 @@ +odoo.define('website.s_chart', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); +const weUtils = require('web_editor.utils'); + +const ChartWidget = publicWidget.Widget.extend({ + selector: '.s_chart', + disabledInEditableMode: false, + jsLibs: [ + '/web/static/lib/Chart/Chart.js', + ], + + /** + * @override + * @param {Object} parent + * @param {Object} options The default value of the chartbar. + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.style = window.getComputedStyle(document.documentElement); + }, + /** + * @override + */ + start: function () { + // Convert Theme colors to css color + const data = JSON.parse(this.el.dataset.data); + data.datasets.forEach(el => { + if (Array.isArray(el.backgroundColor)) { + el.backgroundColor = el.backgroundColor.map(el => this._convertToCssColor(el)); + el.borderColor = el.borderColor.map(el => this._convertToCssColor(el)); + } else { + el.backgroundColor = this._convertToCssColor(el.backgroundColor); + el.borderColor = this._convertToCssColor(el.borderColor); + } + el.borderWidth = this.el.dataset.borderWidth; + }); + + // Make chart data + const chartData = { + type: this.el.dataset.type, + data: data, + options: { + legend: { + display: this.el.dataset.legendPosition !== 'none', + position: this.el.dataset.legendPosition, + }, + tooltips: { + enabled: this.el.dataset.tooltipDisplay === 'true', + }, + title: { + display: !!this.el.dataset.title, + text: this.el.dataset.title, + }, + }, + }; + + // Add type specific options + if (this.el.dataset.type === 'radar') { + chartData.options.scale = { + ticks: { + beginAtZero: true, + } + }; + } else if (['pie', 'doughnut'].includes(this.el.dataset.type)) { + chartData.options.tooltips.callbacks = { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label; + const secondLabel = data.labels[tooltipItem.index]; + let final = label; + if (label) { + if (secondLabel) { + final = label + ' - ' + secondLabel; + } + } else if (secondLabel) { + final = secondLabel; + } + return final + ':' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + }, + }; + } else { + chartData.options.scales = { + xAxes: [{ + stacked: this.el.dataset.stacked === 'true', + ticks: { + beginAtZero: true + }, + }], + yAxes: [{ + stacked: this.el.dataset.stacked === 'true', + ticks: { + beginAtZero: true + }, + }], + }; + } + + // Disable animation in edit mode + if (this.editableMode) { + chartData.options.animation = { + duration: 0, + }; + } + + const canvas = this.el.querySelector('canvas'); + this.chart = new window.Chart(canvas, chartData); + return this._super.apply(this, arguments); + }, + /** + * @override + * Discard all library changes to reset the state of the Html. + */ + destroy: function () { + if (this.chart) { // The widget can be destroyed before start has completed + this.chart.destroy(); + this.el.querySelectorAll('.chartjs-size-monitor').forEach(el => el.remove()); + } + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} color A css color or theme color string + * @returns {string} Css color + */ + _convertToCssColor: function (color) { + if (!color) { + return 'transparent'; + } + return weUtils.getCSSVariableValue(color, this.style) || color; + }, +}); + +publicWidget.registry.chart = ChartWidget; + +return ChartWidget; +}); diff --git a/addons/website/static/src/snippets/s_chart/options.js b/addons/website/static/src/snippets/s_chart/options.js new file mode 100644 index 00000000..fe59cedf --- /dev/null +++ b/addons/website/static/src/snippets/s_chart/options.js @@ -0,0 +1,477 @@ +odoo.define('website.s_chart_options', function (require) { +'use strict'; + +var core = require('web.core'); +const {ColorpickerWidget} = require('web.Colorpicker'); +var options = require('web_editor.snippets.options'); +const weUtils = require('web_editor.utils'); + +var _t = core._t; + +options.registry.InnerChart = options.Class.extend({ + custom_events: _.extend({}, options.Class.prototype.custom_events, { + 'get_custom_colors': '_onGetCustomColors', + }), + events: _.extend({}, options.Class.prototype.events, { + 'click we-button.add_column': '_onAddColumnClick', + 'click we-button.add_row': '_onAddRowClick', + 'click we-button.o_we_matrix_remove_col': '_onRemoveColumnClick', + 'click we-button.o_we_matrix_remove_row': '_onRemoveRowClick', + 'blur we-matrix input': '_onMatrixInputFocusOut', + 'focus we-matrix input': '_onMatrixInputFocus', + }), + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.themeArray = ['o-color-1', 'o-color-2', 'o-color-3', 'o-color-4', 'o-color-5']; + this.style = window.getComputedStyle(document.documentElement); + }, + /** + * @override + */ + start: function () { + this.backSelectEl = this.el.querySelector('[data-name="chart_bg_color_opt"]'); + this.borderSelectEl = this.el.querySelector('[data-name="chart_border_color_opt"]'); + + // Build matrix content + this.tableEl = this.el.querySelector('we-matrix table'); + const data = JSON.parse(this.$target[0].dataset.data); + data.labels.forEach(el => { + this._addRow(el); + }); + data.datasets.forEach((el, i) => { + if (this._isPieChart()) { + // Add header colors in case the user changes the type of graph + const headerBackgroundColor = this.themeArray[i] || this._randomColor(); + const headerBorderColor = this.themeArray[i] || this._randomColor(); + this._addColumn(el.label, el.data, headerBackgroundColor, headerBorderColor, el.backgroundColor, el.borderColor); + } else { + this._addColumn(el.label, el.data, el.backgroundColor, el.borderColor); + } + }); + this._displayRemoveColButton(); + this._displayRemoveRowButton(); + this._setDefaultSelectedInput(); + return this._super(...arguments); + }, + /** + * @override + */ + updateUI: async function () { + // Selected input might not be in dom anymore if col/row removed + // Done before _super because _computeWidgetState of colorChange + if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) { + this._setDefaultSelectedInput(); + } + + await this._super(...arguments); + + // prevent the columns from becoming too small. + this.tableEl.classList.toggle('o_we_matrix_five_col', this.tableEl.querySelectorAll('tr:first-child th').length > 5); + + this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Color") : _t("Dataset Color"); + this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Border") : _t("Dataset Border"); + + // Dataset/Cell color + this.tableEl.querySelectorAll('input').forEach(el => el.style.border = ''); + const selector = this._isPieChart() ? 'td input' : 'tr:first-child input'; + this.tableEl.querySelectorAll(selector).forEach(el => { + const color = el.dataset.backgroundColor || el.dataset.borderColor; + if (color) { + el.style.border = '2px solid'; + el.style.borderColor = ColorpickerWidget.isCSSColor(color) ? color : weUtils.getCSSVariableValue(color, this.style); + } + }); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Set the color on the selected input. + */ + colorChange: async function (previewMode, widgetValue, params) { + if (widgetValue) { + this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue; + } else { + delete this.colorPaletteSelectedInput.dataset[params.attributeName]; + } + await this._reloadGraph(); + // To focus back the input that is edited we have to wait for the color + // picker to be fully reloaded. + await new Promise(resolve => setTimeout(() => { + this.lastEditableSelectedInput.focus(); + resolve(); + })); + }, + /** + * @override + */ + selectDataAttribute: async function (previewMode, widgetValue, params) { + await this._super(...arguments); + // Data might change if going from or to a pieChart. + if (params.attributeName === 'type') { + this._setDefaultSelectedInput(); + await this._reloadGraph(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + if (methodName === 'colorChange') { + return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || ''; + } + return this._super(...arguments); + }, + /** + * @override + */ + _computeWidgetVisibility: function (widgetName, params) { + switch (widgetName) { + case 'stacked_chart_opt': { + return this._getColumnCount() > 1; + } + case 'chart_bg_color_opt': + case 'chart_border_color_opt': { + return !!this.colorPaletteSelectedInput; + } + } + return this._super(...arguments); + }, + /** + * Sets and reloads the data on the canvas if it has changed. + * Used in matrix related method. + * + * @private + */ + _reloadGraph: async function () { + const jsonValue = this._matrixToChartData(); + if (this.$target[0].dataset.data !== jsonValue) { + this.$target[0].dataset.data = jsonValue; + await this._refreshPublicWidgets(); + } + }, + /** + * Return a stringifyed chart.js data object from the matrix + * Pie charts have one color per data while other charts have one color per dataset. + * + * @private + */ + _matrixToChartData: function () { + const data = { + labels: [], + datasets: [], + }; + this.tableEl.querySelectorAll('tr:first-child input').forEach(el => { + data.datasets.push({ + label: el.value || '', + data: [], + backgroundColor: this._isPieChart() ? [] : el.dataset.backgroundColor || '', + borderColor: this._isPieChart() ? [] : el.dataset.borderColor || '', + }); + }); + this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el) => { + const title = el.querySelector('th input').value || ''; + data.labels.push(title); + el.querySelectorAll('td input').forEach((el, i) => { + data.datasets[i].data.push(el.value || 0); + if (this._isPieChart()) { + data.datasets[i].backgroundColor.push(el.dataset.backgroundColor || ''); + data.datasets[i].borderColor.push(el.dataset.borderColor || ''); + } + }); + }); + return JSON.stringify(data); + }, + /** + * Return a td containing a we-button with minus icon + * + * @param {...string} classes Classes to add to the we-button + * @returns {HTMLElement} + */ + _makeDeleteButton: function (...classes) { + const rmbuttonEl = options.buildElement('we-button', null, { + classes: ['o_we_text_danger', 'o_we_link', 'fa', 'fa-fw', 'fa-minus', ...classes], + }); + const newEl = document.createElement('td'); + newEl.appendChild(rmbuttonEl); + return newEl; + }, + /** + * Add a column to the matrix + * The th (dataset label) of a column hold the colors for the entire dataset if the graph is not a pie chart + * If the graph is a pie chart the color of the td (data) are used. + * + * @private + * @param {String} title The title of the column + * @param {Array} values The values of the column input + * @param {String} heardeBackgroundColor The background color of the dataset + * @param {String} headerBorderColor The border color of the dataset + * @param {string[]} cellBackgroundColors The background colors of the datas inputs, random color if missing + * @param {string[]} cellBorderColors The border color of the datas inputs, no color if missing + */ + _addColumn: function (title, values, heardeBackgroundColor, headerBorderColor, cellBackgroundColors = [], cellBorderColors = []) { + const firstRow = this.tableEl.querySelector('tr:first-child'); + const headerInput = this._makeCell('th', title, heardeBackgroundColor, headerBorderColor); + firstRow.insertBefore(headerInput, firstRow.lastElementChild); + + this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el, i) => { + const newCell = this._makeCell('td', values ? values[i] : null, cellBackgroundColors[i] || this._randomColor(), cellBorderColors[i - 1]); + el.insertBefore(newCell, el.lastElementChild); + }); + + const lastRow = this.tableEl.querySelector('tr:last-child'); + const removeButton = this._makeDeleteButton('o_we_matrix_remove_col'); + lastRow.appendChild(removeButton); + }, + /** + * Add a row to the matrix + * The background color of the datas are random + * + * @private + * @param {String} tilte The title of the row + */ + _addRow: function (tilte) { + const trEl = document.createElement('tr'); + trEl.appendChild(this._makeCell('th', tilte)); + this.tableEl.querySelectorAll('tr:first-child input').forEach(() => { + trEl.appendChild(this._makeCell('td', null, this._randomColor())); + }); + trEl.appendChild(this._makeDeleteButton('o_we_matrix_remove_row')); + const tbody = this.tableEl.querySelector('tbody'); + tbody.insertBefore(trEl, tbody.lastElementChild); + }, + /** + * @private + * @param {string} tag tag of the HTML Element (td/th) + * @param {string} value The current value of the cell input + * @param {string} backgroundColor The background Color of the data on the graph + * @param {string} borderColor The border Color of the the data on the graph + * @returns {HTMLElement} + */ + _makeCell: function (tag, value, backgroundColor, borderColor) { + const newEl = document.createElement(tag); + const contentEl = document.createElement('input'); + contentEl.type = 'text'; + contentEl.value = value || ''; + if (backgroundColor) { + contentEl.dataset.backgroundColor = backgroundColor; + } + if (borderColor) { + contentEl.dataset.borderColor = borderColor; + } + newEl.appendChild(contentEl); + return newEl; + }, + /** + * Display the remove button coresponding to the colIndex + * + * @private + * @param {Int} colIndex Can be undefined, if so the last remove button of the column will be shown + */ + _displayRemoveColButton: function (colIndex) { + if (this._getColumnCount() > 1) { + this._displayRemoveButton(colIndex, 'o_we_matrix_remove_col'); + } + }, + /** + * Display the remove button coresponding to the rowIndex + * + * @private + * @param {Int} rowIndex Can be undefined, if so the last remove button of the row will be shown + */ + _displayRemoveRowButton: function (rowIndex) { + //Nbr of row minus header and button + const rowCount = this.tableEl.rows.length - 2; + if (rowCount > 1) { + this._displayRemoveButton(rowIndex, 'o_we_matrix_remove_row'); + } + }, + /** + * @private + * @param {Int} tdIndex Can be undefined, if so the last remove button will be shown + * @param {String} btnClass Either o_we_matrix_remove_col or o_we_matrix_remove_row + */ + _displayRemoveButton: function (tdIndex, btnClass) { + const removeBtn = this.tableEl.querySelectorAll(`td we-button.${btnClass}`); + removeBtn.forEach(el => el.style.display = ''); //hide all + const index = tdIndex < removeBtn.length ? tdIndex : removeBtn.length - 1; + removeBtn[index].style.display = 'inline-block'; + }, + /** + * @private + * @return {boolean} + */ + _isPieChart: function () { + return ['pie', 'doughnut'].includes(this.$target[0].dataset.type); + }, + /** + * Return the number of column minus header and button + * @private + * @return {integer} + */ + _getColumnCount: function () { + return this.tableEl.rows[0].cells.length - 2; + }, + /** + * Select the first data input + * + * @private + */ + _setDefaultSelectedInput: function () { + this.lastEditableSelectedInput = this.tableEl.querySelector('td input'); + if (this._isPieChart()) { + this.colorPaletteSelectedInput = this.lastEditableSelectedInput; + } else { + this.colorPaletteSelectedInput = this.tableEl.querySelector('th input'); + } + }, + /** + * Return a random hexadecimal color. + * + * @private + * @return {string} + */ + _randomColor: function () { + return '#' + ('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6).toUpperCase(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Used by colorPalette to retrieve the custom colors used on the chart + * Make an array with all the custom colors used on the chart + * and apply it to the onSuccess method provided by the trigger_up. + * + * @private + */ + _onGetCustomColors: function (ev) { + const data = JSON.parse(this.$target[0].dataset.data || ''); + let customColors = []; + data.datasets.forEach(el => { + if (this._isPieChart()) { + customColors = customColors.concat(el.backgroundColor).concat(el.borderColor); + } else { + customColors.push(el.backgroundColor); + customColors.push(el.borderColor); + } + }); + customColors = customColors.filter((el, i, array) => { + return !weUtils.getCSSVariableValue(el, this.style) && array.indexOf(el) === i && el !== ''; // unique non class not transparent + }); + ev.data.onSuccess(customColors); + }, + /** + * Add a row at the end of the matrix and display it's remove button + * Choose the color of the column from the theme array or a random color if they are already used + * + * @private + */ + _onAddColumnClick: function () { + const usedColor = Array.from(this.tableEl.querySelectorAll('tr:first-child input')).map(el => el.dataset.backgroundColor); + const color = this.themeArray.filter(el => !usedColor.includes(el))[0] || this._randomColor(); + this._addColumn(null, null, color, color); + this._reloadGraph().then(() => { + this._displayRemoveColButton(); + this.updateUI(); + }); + }, + /** + * Add a column at the end of the matrix and display it's remove button + * + * @private + */ + _onAddRowClick: function () { + this._addRow(); + this._reloadGraph().then(() => { + this._displayRemoveRowButton(); + this.updateUI(); + }); + }, + /** + * Remove the column and show the remove button of the next column or the last if no next. + * + * @private + * @param {Event} ev + */ + _onRemoveColumnClick: function (ev) { + const cell = ev.currentTarget.parentElement; + const cellIndex = cell.cellIndex; + this.tableEl.querySelectorAll('tr').forEach((el) => { + el.children[cellIndex].remove(); + }); + this._displayRemoveColButton(cellIndex - 1); + this._reloadGraph().then(() => { + this.updateUI(); + }); + }, + /** + * Remove the row and show the remove button of the next row or the last if no next. + * + * @private + * @param {Event} ev + */ + _onRemoveRowClick: function (ev) { + const row = ev.currentTarget.parentElement.parentElement; + const rowIndex = row.rowIndex; + row.remove(); + this._displayRemoveRowButton(rowIndex - 1); + this._reloadGraph().then(() => { + this.updateUI(); + }); + }, + /** + * @private + * @param {Event} ev + */ + _onMatrixInputFocusOut: function (ev) { + // Sometimes, an input is focusout for internal reason (like an undo + // recording) then focused again manually in the same JS stack + // execution. In that case, the blur should not trigger an option + // selection as the user did not leave the input. We thus defer the blur + // handling to then check that the target is indeed still blurred before + // executing the actual option selection. + setTimeout(() => { + if (ev.currentTarget === document.activeElement) { + return; + } + this._reloadGraph(); + }); + }, + /** + * Set the selected cell/header and display the related remove button + * + * @private + * @param {Event} ev + */ + _onMatrixInputFocus: function (ev) { + this.lastEditableSelectedInput = ev.target; + const col = ev.target.parentElement.cellIndex; + const row = ev.target.parentElement.parentElement.rowIndex; + if (this._isPieChart()) { + this.colorPaletteSelectedInput = ev.target.parentNode.tagName === 'TD' ? ev.target : null; + } else { + this.colorPaletteSelectedInput = this.tableEl.querySelector(`tr:first-child th:nth-of-type(${col + 1}) input`); + } + if (col > 0) { + this._displayRemoveColButton(col - 1); + } + if (row > 0) { + this._displayRemoveRowButton(row - 1); + } + this.updateUI(); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_color_blocks_2/000.scss b/addons/website/static/src/snippets/s_color_blocks_2/000.scss new file mode 100644 index 00000000..555cab58 --- /dev/null +++ b/addons/website/static/src/snippets/s_color_blocks_2/000.scss @@ -0,0 +1,36 @@ +.s_color_blocks_2 { + // Needed to be able to stretch the inner container so that + // the snippet works with the 50% and 100% height + &.o_half_screen_height, &.o_full_screen_height { + > :first-child { // container + &, > .row { + min-height: inherit; + } + } + } + .row { + display: flex; + flex-flow: row wrap; + + // Fix for safari browser as it 'supports' flex but not with the right + // behavior + &::before, &::after { + width: 0; + } + } + [class*="col-lg-"] { + padding: 8% 5%; + padding-top: 8vw; // A flex item cannot have % padding top and bottom (even if it works on chrome) + padding-bottom: 8vw; // Solution is vw units but we keep 8% as a fallback + } + @include media-breakpoint-down(md) { + [class*="col-lg-"] { + flex: 1 1 100%; + } + } + + img { + max-width: 100%; + height: auto; + } +} diff --git a/addons/website/static/src/snippets/s_company_team/000.scss b/addons/website/static/src/snippets/s_company_team/000.scss new file mode 100644 index 00000000..7947b831 --- /dev/null +++ b/addons/website/static/src/snippets/s_company_team/000.scss @@ -0,0 +1,8 @@ + +.s_company_team { + @include media-breakpoint-down(md) { + img { + max-width: 50%; + } + } +} diff --git a/addons/website/static/src/snippets/s_comparisons/000.scss b/addons/website/static/src/snippets/s_comparisons/000.scss new file mode 100644 index 00000000..cf1e9218 --- /dev/null +++ b/addons/website/static/src/snippets/s_comparisons/000.scss @@ -0,0 +1,21 @@ + +.s_comparisons { + .card-body { + .card-title { + margin: 0; + } + .s_comparisons_currency, + .s_comparisons_price, + .s_comparisons_decimal { + display: inline-block; + vertical-align: middle; + } + .s_comparisons_currency, + .s_comparisons_decimal { + font-size: 80%; + } + .s_comparisons_price { + font-size: 200%; + } + } +} diff --git a/addons/website/static/src/snippets/s_countdown/000.js b/addons/website/static/src/snippets/s_countdown/000.js new file mode 100644 index 00000000..fcdac7b7 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/000.js @@ -0,0 +1,422 @@ +odoo.define('website.s_countdown', function (require) { +'use strict'; + +const {ColorpickerWidget} = require('web.Colorpicker'); +const core = require('web.core'); +const publicWidget = require('web.public.widget'); +const weUtils = require('web_editor.utils'); + +const qweb = core.qweb; +const _t = core._t; + +const CountdownWidget = publicWidget.Widget.extend({ + selector: '.s_countdown', + xmlDependencies: ['/website/static/src/snippets/s_countdown/000.xml'], + disabledInEditableMode: false, + defaultColor: 'rgba(0, 0, 0, 255)', + + /** + * @override + */ + start: function () { + this.$wrapper = this.$('.s_countdown_canvas_wrapper'); + this.hereBeforeTimerEnds = false; + this.endAction = this.el.dataset.endAction; + this.endTime = parseInt(this.el.dataset.endTime); + this.size = parseInt(this.el.dataset.size); + this.display = this.el.dataset.display; + + this.layout = this.el.dataset.layout; + this.layoutBackground = this.el.dataset.layoutBackground; + this.progressBarStyle = this.el.dataset.progressBarStyle; + this.progressBarWeight = this.el.dataset.progressBarWeight; + + this.textColor = this._ensureCssColor(this.el.dataset.textColor); + this.layoutBackgroundColor = this._ensureCssColor(this.el.dataset.layoutBackgroundColor); + this.progressBarColor = this._ensureCssColor(this.el.dataset.progressBarColor); + + this.onlyOneUnit = this.display === 'd'; + this.width = parseInt(this.size); + if (this.layout === 'boxes') { + this.width /= 1.75; + } + this._initTimeDiff(); + + this._render(); + + this.setInterval = setInterval(this._render.bind(this), 1000); + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this.$('.s_countdown_end_redirect_message').remove(); + this.$('canvas').remove(); + this.$('.s_countdown_end_message').addClass('d-none'); + this.$('.s_countdown_text_wrapper').remove(); + this.$('.s_countdown_canvas_wrapper').removeClass('d-none'); + + clearInterval(this.setInterval); + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Ensures the color is an actual css color. In case of a color variable, + * the color will be mapped to hexa. + * + * @private + * @param {string} color + * @returns {string} + */ + _ensureCssColor: function (color) { + if (ColorpickerWidget.isCSSColor(color)) { + return color; + } + return weUtils.getCSSVariableValue(color) || this.defaultColor; + }, + /** + * Gets the time difference in seconds between now and countdown due date. + * + * @private + */ + _getDelta: function () { + const currentTimestamp = Date.now() / 1000; + return this.endTime - currentTimestamp; + }, + /** + * Handles the action that should be executed once the countdown ends. + * + * @private + */ + _handleEndCountdownAction: function () { + if (this.endAction === 'redirect') { + const redirectUrl = this.el.dataset.redirectUrl || '/'; + if (this.hereBeforeTimerEnds) { + // Wait a bit, if the landing page has the same publish date + setTimeout(() => window.location = redirectUrl, 500); + } else { + // Show (non editable) msg when user lands on already finished countdown + if (!this.$('.s_countdown_end_redirect_message').length) { + const $container = this.$('> .container, > .container-fluid, > .o_container_small'); + $container.append( + $(qweb.render('website.s_countdown.end_redirect_message', { + redirectUrl: redirectUrl, + })) + ); + } + } + } else if (this.endAction === 'message') { + this.$('.s_countdown_end_message').removeClass('d-none'); + } + }, + /** + * Initializes the `diff` object. It will contains every visible time unit + * which will each contain its related canvas, total step, label.. + * + * @private + */ + _initTimeDiff: function () { + const delta = this._getDelta(); + this.diff = []; + if (this._isUnitVisible('d') && !(this.onlyOneUnit && delta < 86400)) { + this.diff.push({ + canvas: $('<canvas/>', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + // There is no logical number of unit (total) on which day units + // can be compared against, so we use an arbitrary number. + total: 15, + label: _t("Days"), + nbSeconds: 86400, + }); + } + if (this._isUnitVisible('h') || (this.onlyOneUnit && delta < 86400 && delta > 3600)) { + this.diff.push({ + canvas: $('<canvas/>', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + total: 24, + label: _t("Hours"), + nbSeconds: 3600, + }); + } + if (this._isUnitVisible('m') || (this.onlyOneUnit && delta < 3600 && delta > 60)) { + this.diff.push({ + canvas: $('<canvas/>', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + total: 60, + label: _t("Minutes"), + nbSeconds: 60, + }); + } + if (this._isUnitVisible('s') || (this.onlyOneUnit && delta < 60)) { + this.diff.push({ + canvas: $('<canvas/>', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + total: 60, + label: _t("Seconds"), + nbSeconds: 1, + }); + } + }, + /** + * Returns weither or not the countdown should be displayed for the given + * unit (days, sec..). + * + * @private + * @param {string} unit - either 'd', 'm', 'h', or 's' + * @returns {boolean} + */ + _isUnitVisible: function (unit) { + return this.display.includes(unit); + }, + /** + * Draws the whole countdown, including one countdown for each time unit. + * + * @private + */ + _render: function () { + // If only one unit mode, restart widget on unit change to populate diff + if (this.onlyOneUnit && this._getDelta() < this.diff[0].nbSeconds) { + this.$('canvas').remove(); + this._initTimeDiff(); + } + this._updateTimeDiff(); + + const hideCountdown = this.isFinished && !this.editableMode && this.$el.hasClass('hide-countdown'); + if (this.layout === 'text') { + this.$('canvas').addClass('d-none'); + if (!this.$textWrapper) { + this.$textWrapper = $('<span/>').attr({ + class: 's_countdown_text_wrapper d-none', + }); + this.$textWrapper.text(_t("Countdown ends in")); + this.$textWrapper.append($('<span/>').attr({ + class: 's_countdown_text ml-1', + })); + this.$textWrapper.appendTo(this.$wrapper); + } + + this.$textWrapper.toggleClass('d-none', hideCountdown); + + const countdownText = this.diff.map(e => e.nb + ' ' + e.label).join(', '); + this.$('.s_countdown_text').text(countdownText.toLowerCase()); + } else { + for (const val of this.diff) { + const canvas = val.canvas; + const ctx = canvas.getContext("2d"); + ctx.canvas.width = this.width; + ctx.canvas.height = this.size; + this._clearCanvas(ctx); + + $(canvas).toggleClass('d-none', hideCountdown); + if (hideCountdown) { + continue; + } + + // Draw canvas elements + if (this.layoutBackground !== 'none') { + this._drawBgShape(ctx, this.layoutBackground === 'plain'); + } + this._drawText(canvas, val.nb, val.label, this.layoutBackground === 'plain'); + if (this.progressBarStyle === 'surrounded') { + this._drawProgressBarBg(ctx, this.progressBarWeight === 'thin'); + } + if (this.progressBarStyle !== 'none') { + this._drawProgressBar(ctx, val.nb, val.total, this.progressBarWeight === 'thin'); + } + $(canvas).toggleClass('mx-2', this.layout === 'boxes'); + } + } + + if (this.isFinished) { + clearInterval(this.setInterval); + if (!this.editableMode) { + this._handleEndCountdownAction(); + } + } + }, + /** + * Updates the remaining units into the `diff` object. + * + * @private + */ + _updateTimeDiff: function () { + let delta = this._getDelta(); + this.isFinished = delta < 0; + if (this.isFinished) { + for (const unitData of this.diff) { + unitData.nb = 0; + } + return; + } + + this.hereBeforeTimerEnds = true; + for (const unitData of this.diff) { + unitData.nb = Math.floor(delta / unitData.nbSeconds); + delta -= unitData.nb * unitData.nbSeconds; + } + }, + + //-------------------------------------------------------------------------- + // Canvas drawing methods + //-------------------------------------------------------------------------- + + /** + * Erases the canvas. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + */ + _clearCanvas: function (ctx) { + ctx.clearRect(0, 0, this.size, this.size); + }, + /** + * Draws a text into the canvas. + * + * @private + * @param {HTMLCanvasElement} canvas + * @param {string} textNb - text to display in the center of the canvas, in big + * @param {string} textUnit - text to display bellow `textNb` in small + * @param {boolean} full - if true, the shape will be drawn up to the progressbar + */ + _drawText: function (canvas, textNb, textUnit, full = false) { + const ctx = canvas.getContext("2d"); + const nbSize = this.size / 4; + ctx.font = `${nbSize}px Arial`; + ctx.fillStyle = this.textColor; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(textNb, canvas.width / 2, canvas.height / 2); + + const unitSize = this.size / 12; + ctx.font = `${unitSize}px Arial`; + ctx.fillText(textUnit, canvas.width / 2, canvas.height / 2 + nbSize / 1.5, this.width); + + if (this.layout === 'boxes' && this.layoutBackground !== 'none' && this.progressBarStyle === 'none') { + let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10); + if (full) { + barWidth = 0; + } + ctx.beginPath(); + ctx.moveTo(barWidth, this.size / 2); + ctx.lineTo(this.width - barWidth, this.size / 2); + ctx.stroke(); + } + }, + /** + * Draws a plain shape into the canvas. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + * @param {boolean} full - if true, the shape will be drawn up to the progressbar + */ + _drawBgShape: function (ctx, full = false) { + ctx.fillStyle = this.layoutBackgroundColor; + ctx.beginPath(); + if (this.layout === 'circle') { + let rayon = this.size / 2; + if (this.progressBarWeight === 'thin') { + rayon -= full ? this.size / 29 : this.size / 15; + } else { + rayon -= full ? 0 : this.size / 10; + } + ctx.arc(this.size / 2, this.size / 2, rayon, 0, Math.PI * 2); + ctx.fill(); + } else if (this.layout === 'boxes') { + let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10); + if (full) { + barWidth = 0; + } + + ctx.fillStyle = this.layoutBackgroundColor; + ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2); + ctx.fill(); + + const gradient = ctx.createLinearGradient(0, this.width, 0, 0); + gradient.addColorStop(0, '#ffffff24'); + gradient.addColorStop(1, this.layoutBackgroundColor); + ctx.fillStyle = gradient; + ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2); + ctx.fill(); + $(ctx.canvas).css({'border-radius': '8px'}); + } + }, + /** + * Draws a progress bar around the countdown shape. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + * @param {string} nbUnit - how many unit should fill progress bar + * @param {string} totalUnit - number of unit to do a complete progress bar + * @param {boolean} thinLine - if true, the progress bar will be thiner + */ + _drawProgressBar: function (ctx, nbUnit, totalUnit, thinLine) { + ctx.strokeStyle = this.progressBarColor; + ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10; + if (this.layout === 'circle') { + ctx.beginPath(); + ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, Math.PI / -2, (Math.PI * 2) * (nbUnit / totalUnit) + (Math.PI / -2)); + ctx.stroke(); + } else if (this.layout === 'boxes') { + ctx.lineWidth *= 2; + let pc = nbUnit / totalUnit * 100; + + // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2) + const linesCoordFuncs = [ + (linePc) => [0 + ctx.lineWidth / 2, 0, (this.width - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2, 0], + (linePc) => [this.width, 0 + ctx.lineWidth / 2, this.width, (this.size - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2], + (linePc) => [this.width - ((this.width - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, this.size, this.width - ctx.lineWidth / 2, this.size], + (linePc) => [0, this.size - ((this.size - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, 0, this.size - ctx.lineWidth / 2], + ]; + while (pc > 0 && linesCoordFuncs.length) { + const linePc = Math.min(pc, 25); + const lineCoord = (linesCoordFuncs.shift())(linePc); + ctx.beginPath(); + ctx.moveTo(lineCoord[0], lineCoord[1]); + ctx.lineTo(lineCoord[2], lineCoord[3]); + ctx.stroke(); + pc -= linePc; + } + } + }, + /** + * Draws a full lighter background progressbar around the shape. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + * @param {boolean} thinLine - if true, the progress bar will be thiner + */ + _drawProgressBarBg: function (ctx, thinLine) { + ctx.strokeStyle = this.progressBarColor; + ctx.globalAlpha = 0.2; + ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10; + if (this.layout === 'circle') { + ctx.beginPath(); + ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, 0, Math.PI * 2); + ctx.stroke(); + } else if (this.layout === 'boxes') { + ctx.lineWidth *= 2; + + // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2) + const points = [ + [0 + ctx.lineWidth / 2, 0, this.width, 0], + [this.width, 0 + ctx.lineWidth / 2, this.width, this.size], + [0, this.size, this.width - ctx.lineWidth / 2, this.size], + [0, 0, 0, this.size - ctx.lineWidth / 2], + ]; + while (points.length) { + const point = points.shift(); + ctx.beginPath(); + ctx.moveTo(point[0], point[1]); + ctx.lineTo(point[2], point[3]); + ctx.stroke(); + } + } + ctx.globalAlpha = 1; + }, +}); + +publicWidget.registry.countdown = CountdownWidget; + +return CountdownWidget; +}); diff --git a/addons/website/static/src/snippets/s_countdown/000.xml b/addons/website/static/src/snippets/s_countdown/000.xml new file mode 100644 index 00000000..07905447 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/000.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="website.s_countdown.end_redirect_message"> + <p class="text-center s_countdown_end_redirect_message">Time's up! You can now visit <a class="s_countdown_end_redirect_url" t-attf-href="#{redirectUrl}">this page</a>.</p> + </t> + <t t-name="website.s_countdown.end_message"> + <div class="s_countdown_end_message d-none"> + <div class="text-center alert alert-info css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status"> + The following message will become visible <strong>only</strong> once the countdown ends. + </div> + <div class="oe_structure"> + <section class="s_picture bg-200 pt48 pb24" data-snippet="s_picture"> + <div class="container"> + <h2 style="text-align: center;">Happy Odoo Anniversary!</h2> + <p style="text-align: center;">As promised, we will offer 4 free tickets to our next summit.<br/>Visit our Facebook page to know if you are one of the lucky winners.</p> + <p><br/></p> + <div class="row s_nb_column_fixed"> + <div class="col-lg-12 pb24"> + <figure class="figure"> + <img src="/web/image/website.library_image_18" class="figure-img img-thumbnail mx-auto padding-large" style="width: 50%;" alt="Countdown is over - Firework"/> + </figure> + </div> + </div> + </div> + </section> + </div> + </div> + </t> +</templates> diff --git a/addons/website/static/src/snippets/s_countdown/options.js b/addons/website/static/src/snippets/s_countdown/options.js new file mode 100644 index 00000000..ee99e0a8 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/options.js @@ -0,0 +1,135 @@ +odoo.define('website.s_countdown_options', function (require) { +'use strict'; + +const core = require('web.core'); +const options = require('web_editor.snippets.options'); +const CountdownWidget = require('website.s_countdown'); + +const qweb = core.qweb; + +options.registry.countdown = options.Class.extend({ + events: _.extend({}, options.Class.prototype.events || {}, { + 'click .toggle-edit-message': '_onToggleEndMessageClick', + }), + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Changes the countdown action at zero. + * + * @see this.selectClass for parameters + */ + endAction: function (previewMode, widgetValue, params) { + this.$target[0].dataset.endAction = widgetValue; + if (widgetValue === 'message') { + if (!this.$target.find('.s_countdown_end_message').length) { + const message = this.endMessage || qweb.render('website.s_countdown.end_message'); + this.$target.append(message); + } + } else { + const $message = this.$target.find('.s_countdown_end_message').detach(); + if ($message.length) { + this.endMessage = $message[0].outerHTML; + } + } + }, + /** + * Changes the countdown style. + * + * @see this.selectClass for parameters + */ + layout: function (previewMode, widgetValue, params) { + switch (widgetValue) { + case 'circle': + this.$target[0].dataset.progressBarStyle = 'disappear'; + this.$target[0].dataset.progressBarWeight = 'thin'; + this.$target[0].dataset.layoutBackground = 'none'; + break; + case 'boxes': + this.$target[0].dataset.progressBarStyle = 'none'; + this.$target[0].dataset.layoutBackground = 'plain'; + break; + case 'clean': + this.$target[0].dataset.progressBarStyle = 'none'; + this.$target[0].dataset.layoutBackground = 'none'; + break; + case 'text': + this.$target[0].dataset.progressBarStyle = 'none'; + this.$target[0].dataset.layoutBackground = 'none'; + break; + } + this.$target[0].dataset.layout = widgetValue; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + updateUIVisibility: async function () { + await this._super(...arguments); + const dataset = this.$target[0].dataset; + + // End Action UI + this.$el.find('.toggle-edit-message') + .toggleClass('d-none', dataset.endAction !== 'message'); + + // End Message UI + this.updateUIEndMessage(); + }, + /** + * @see this.updateUI + */ + updateUIEndMessage: function () { + this.$target.find('.s_countdown_canvas_wrapper') + .toggleClass("d-none", this.showEndMessage === true && this.$target.hasClass("hide-countdown")); + this.$target.find('.s_countdown_end_message') + .toggleClass("d-none", !this.showEndMessage); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'endAction': + case 'layout': + return this.$target[0].dataset[methodName]; + + case 'selectDataAttribute': { + if (params.colorNames) { + // In this case, it is a colorpicker controlling a data + // value on the countdown: the default value is determined + // by the countdown public widget. + params.attributeDefaultValue = CountdownWidget.prototype.defaultColor; + } + break; + } + } + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onToggleEndMessageClick: function () { + this.showEndMessage = !this.showEndMessage; + this.$el.find(".toggle-edit-message") + .toggleClass('text-primary', this.showEndMessage); + this.updateUIEndMessage(); + this.trigger_up('cover_update'); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.js b/addons/website/static/src/snippets/s_dynamic_snippet/000.js new file mode 100644 index 00000000..d6a3e0ff --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.js @@ -0,0 +1,244 @@ +odoo.define('website.s_dynamic_snippet', function (require) { +'use strict'; + +const core = require('web.core'); +const config = require('web.config'); +const publicWidget = require('web.public.widget'); + +const DynamicSnippet = publicWidget.Widget.extend({ + selector: '.s_dynamic_snippet', + xmlDependencies: ['/website/static/src/snippets/s_dynamic_snippet/000.xml'], + read_events: { + 'click [data-url]': '_onCallToAction', + }, + disabledInEditableMode: false, + + /** + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + /** + * The dynamic filter data source data formatted with the chosen template. + * Can be accessed when overriding the _render_content() function in order to generate + * a new renderedContent from the original data. + * + * @type {*|jQuery.fn.init|jQuery|HTMLElement} + */ + this.data = []; + this.renderedContent = ''; + this.isDesplayedAsMobile = config.device.isMobile; + this.uniqueId = _.uniqueId('s_dynamic_snippet_'); + this.template_key = 'website.s_dynamic_snippet.grid'; + }, + /** + * + * @override + */ + willStart: function () { + return this._super.apply(this, arguments).then( + () => Promise.all([ + this._fetchData(), + this._manageWarningMessageVisibility() + ]) + ); + }, + /** + * + * @override + */ + start: function () { + return this._super.apply(this, arguments) + .then(() => { + this._setupSizeChangedManagement(true); + this._render(); + this._toggleVisibility(true); + }); + }, + /** + * + * @override + */ + destroy: function () { + this._toggleVisibility(false); + this._setupSizeChangedManagement(false); + this._clearContent(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * + * @private + */ + _clearContent: function () { + const $dynamicSnippetTemplate = this.$el.find('.dynamic_snippet_template'); + if ($dynamicSnippetTemplate) { + $dynamicSnippetTemplate.html(''); + } + }, + /** + * Method to be overridden in child components if additional configuration elements + * are required in order to fetch data. + * @private + */ + _isConfigComplete: function () { + return this.$el.get(0).dataset.filterId !== undefined && this.$el.get(0).dataset.templateKey !== undefined; + }, + /** + * Method to be overridden in child components in order to provide a search + * domain if needed. + * @private + */ + _getSearchDomain: function () { + return []; + }, + /** + * Fetches the data. + * @private + */ + _fetchData: function () { + if (this._isConfigComplete()) { + return this._rpc( + { + 'route': '/website/snippet/filters', + 'params': { + 'filter_id': parseInt(this.$el.get(0).dataset.filterId), + 'template_key': this.$el.get(0).dataset.templateKey, + 'limit': parseInt(this.$el.get(0).dataset.numberOfRecords), + 'search_domain': this._getSearchDomain() + }, + }) + .then( + (data) => { + this.data = data; + } + ); + } else { + return new Promise((resolve) => { + this.data = []; + resolve(); + }); + } + }, + /** + * + * @private + */ + _mustMessageWarningBeHidden: function() { + return this._isConfigComplete() || !this.editableMode; + }, + /** + * + * @private + */ + _manageWarningMessageVisibility: async function () { + this.$el.find('.missing_option_warning').toggleClass( + 'd-none', + this._mustMessageWarningBeHidden() + ); + }, + /** + * Method to be overridden in child components in order to prepare content + * before rendering. + * @private + */ + _prepareContent: function () { + if (this.$target[0].dataset.numberOfElements && this.$target[0].dataset.numberOfElementsSmallDevices) { + this.renderedContent = core.qweb.render( + this.template_key, + this._getQWebRenderOptions()); + } else { + this.renderedContent = ''; + } + }, + /** + * Method to be overridden in child components in order to prepare QWeb + * options. + * @private + */ + _getQWebRenderOptions: function () { + return { + chunkSize: parseInt( + config.device.isMobile + ? this.$target[0].dataset.numberOfElementsSmallDevices + : this.$target[0].dataset.numberOfElements + ), + data: this.data, + uniqueId: this.uniqueId + }; + }, + /** + * + * @private + */ + _render: function () { + if (this.data.length) { + this._prepareContent(); + } else { + this.renderedContent = ''; + } + this._renderContent(); + }, + /** + * + * @private + */ + _renderContent: function () { + this.$el.find('.dynamic_snippet_template').html(this.renderedContent); + }, + /** + * + * @param {Boolean} enable + * @private + */ + _setupSizeChangedManagement: function (enable) { + if (enable === true) { + config.device.bus.on('size_changed', this, this._onSizeChanged); + } else { + config.device.bus.off('size_changed', this, this._onSizeChanged); + } + }, + /** + * + * @param visible + * @private + */ + _toggleVisibility: function (visible) { + this.$el.toggleClass('d-none', !visible); + }, + + //------------------------------------- ------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Navigates to the call to action url. + * @private + */ + _onCallToAction: function (ev) { + window.location = $(ev.currentTarget).attr('data-url'); + }, + /** + * Called when the size has reached a new bootstrap breakpoint. + * + * @private + * @param {number} size as Integer @see web.config.device.SIZES + */ + _onSizeChanged: function (size) { + if (this.isDesplayedAsMobile !== config.device.isMobile) { + this.isDesplayedAsMobile = config.device.isMobile; + this._render(); + } + }, +}); + +publicWidget.registry.dynamic_snippet = DynamicSnippet; + +return DynamicSnippet; + +}); diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.scss b/addons/website/static/src/snippets/s_dynamic_snippet/000.scss new file mode 100644 index 00000000..536aba85 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.scss @@ -0,0 +1,11 @@ +.s_dynamic { + [data-url] { + cursor: pointer; + } + .card-img-top { + height: 12rem; + } + img { + object-fit: scale-down; + } +} diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.xml b/addons/website/static/src/snippets/s_dynamic_snippet/000.xml new file mode 100644 index 00000000..105078e0 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-name="website.s_dynamic_snippet.grid"> + <!-- Content --> + <t t-set="colClass" t-value="'col-' + (12 / chunkSize).toString()"/> + <t t-set="rowIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/chunkSize)).keys())"/> + <t t-set="colIndexGenerator" t-value="Array.from(Array(chunkSize).keys())"/> + <t t-foreach="rowIndexGenerator" t-as="rowIndex"> + <div class="row my-4"> + <t t-foreach="colIndexGenerator" t-as="colIndex"> + <t t-if="(rowIndex * chunkSize + colIndex) < data.length"> + <div t-attf-class="#{colClass}"> + <t t-raw="data[rowIndex * chunkSize + colIndex]"/> + </div> + </t> + </t> + </div> + </t> + </t> +</templates> diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/options.js b/addons/website/static/src/snippets/s_dynamic_snippet/options.js new file mode 100644 index 00000000..2cbdcb1c --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/options.js @@ -0,0 +1,136 @@ +odoo.define('website.s_dynamic_snippet_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +const dynamicSnippetOptions = options.Class.extend({ + + /** + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.dynamicFilters = {}; + this.dynamicFilterTemplates = {}; + }, + /** + * + * @override + */ + onBuilt: function () { + this._setOptionsDefaultValues(); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * + * @see this.selectClass for parameters + */ + selectDataAttribute: function (previewMode, widgetValue, params) { + this._super.apply(this, arguments); + if (params.attributeName === 'filterId' && previewMode === false) { + this.$target.get(0).dataset.numberOfRecords = this.dynamicFilters[parseInt(widgetValue)].limit; + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Fetches dynamic filters. + * @private + * @returns {Promise} + */ + _fetchDynamicFilters: function () { + return this._rpc({route: '/website/snippet/options_filters'}); + }, + /** + * Fetch dynamic filters templates. + * @private + * @returns {Promise} + */ + _fetchDynamicFilterTemplates: function () { + return this._rpc({route: '/website/snippet/filter_templates'}); + }, + /** + * + * @override + * @private + */ + _renderCustomXML: async function (uiFragment) { + await this._renderDynamicFiltersSelector(uiFragment); + await this._renderDynamicFilterTemplatesSelector(uiFragment); + }, + /** + * Renders the dynamic filter option selector content into the provided uiFragment. + * @param {HTMLElement} uiFragment + * @private + */ + _renderDynamicFiltersSelector: async function (uiFragment) { + const dynamicFilters = await this._fetchDynamicFilters(); + for (let index in dynamicFilters) { + this.dynamicFilters[dynamicFilters[index].id] = dynamicFilters[index]; + } + const filtersSelectorEl = uiFragment.querySelector('[data-name="filter_opt"]'); + return this._renderSelectUserValueWidgetButtons(filtersSelectorEl, this.dynamicFilters); + }, + /** + * Renders we-buttons into a SelectUserValueWidget element according to provided data. + * @param {HTMLElement} selectUserValueWidgetElement the SelectUserValueWidget buttons + * have to be created into. + * @param {JSON} data + * @private + */ + _renderSelectUserValueWidgetButtons: async function (selectUserValueWidgetElement, data) { + for (let id in data) { + const button = document.createElement('we-button'); + button.dataset.selectDataAttribute = id; + button.innerHTML = data[id].name; + selectUserValueWidgetElement.appendChild(button); + } + }, + /** + * Renders the template option selector content into the provided uiFragment. + * @param {HTMLElement} uiFragment + * @private + */ + _renderDynamicFilterTemplatesSelector: async function (uiFragment) { + const dynamicFilterTemplates = await this._fetchDynamicFilterTemplates(); + for (let index in dynamicFilterTemplates) { + this.dynamicFilterTemplates[dynamicFilterTemplates[index].key] = dynamicFilterTemplates[index]; + } + const templatesSelectorEl = uiFragment.querySelector('[data-name="template_opt"]'); + return this._renderSelectUserValueWidgetButtons(templatesSelectorEl, this.dynamicFilterTemplates); + }, + /** + * Sets default options values. + * Method to be overridden in child components in order to set additional + * options default values. + * @private + */ + _setOptionsDefaultValues: function () { + this._setOptionValue('numberOfElements', 4); + this._setOptionValue('numberOfElementsSmallDevices', 1); + }, + /** + * Sets the option value. + * @param optionName + * @param value + * @private + */ + _setOptionValue: function (optionName, value) { + if (this.$target.get(0).dataset[optionName] === undefined) { + this.$target.get(0).dataset[optionName] = value; + } + }, +}); + +options.registry.dynamic_snippet = dynamicSnippetOptions; + +return dynamicSnippetOptions; +}); diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js new file mode 100644 index 00000000..cf43100a --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js @@ -0,0 +1,46 @@ +odoo.define('website.s_dynamic_snippet_carousel', function (require) { +'use strict'; + +const config = require('web.config'); +const core = require('web.core'); +const publicWidget = require('web.public.widget'); +const DynamicSnippet = require('website.s_dynamic_snippet'); + +const DynamicSnippetCarousel = DynamicSnippet.extend({ + selector: '.s_dynamic_snippet_carousel', + xmlDependencies: (DynamicSnippet.prototype.xmlDependencies || []).concat( + ['/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml'] + ), + + /** + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.template_key = 'website.s_dynamic_snippet.carousel'; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Method to be overridden in child components in order to prepare QWeb + * options + * @private + */ + _getQWebRenderParams: function () { + return Object.assign( + this._super.apply(this, arguments), + { + interval : parseInt(this.$target[0].dataset.carouselInterval), + }, + ); + }, + +}); +publicWidget.registry.dynamic_snippet_carousel = DynamicSnippetCarousel; + +return DynamicSnippetCarousel; +}); diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss new file mode 100644 index 00000000..3eceb172 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss @@ -0,0 +1,11 @@ +.s_dynamic { + .carousel-control-prev, .carousel-control-next { + position: absolute; + width: 4rem; + + > span.fa { + color: gray('700'); + background: radial-gradient($white 50%, transparent 50%); + } + } +} diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml new file mode 100644 index 00000000..1efb8f22 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-name="website.s_dynamic_snippet.carousel"> + <div t-att-id="uniqueId" class="carousel slide" t-att-data-interval="interval"> + <!-- Content --> + <div class="carousel-inner row w-100 mx-auto" role="listbox"> + <t t-set="colClass" t-value="'col-' + (12 / chunkSize).toString()"/> + <t t-set="slideIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/chunkSize)).keys())"/> + <t t-set="itemIndexGenerator" t-value="Array.from(Array(chunkSize).keys())"/> + <t t-foreach="slideIndexGenerator" t-as="slideIndex"> + <div t-attf-class="carousel-item #{slideIndex_first ? 'active' : ''}"> + <div class="row"> + <t t-foreach="itemIndexGenerator" t-as="itemIndex"> + <t t-if="(slideIndex * chunkSize + itemIndex) < data.length"> + <div t-attf-class="#{colClass}"> + <t t-raw="data[slideIndex * chunkSize + itemIndex]"/> + </div> + </t> + </t> + </div> + </div> + </t> + </div> + <!-- Controls --> + <a t-attf-href="##{uniqueId}" class="carousel-control-prev" data-slide="prev" role="button" aria-label="Previous" title="Previous"> + <span class="fa fa-chevron-circle-left fa-2x"/> + <span class="sr-only">Previous</span> + </a> + <a t-attf-href="##{uniqueId}" class="carousel-control-next" data-slide="next" role="button" aria-label="Next" title="Next"> + <span class="fa fa-chevron-circle-right fa-2x"/> + <span class="sr-only">Next</span> + </a> + </div> + </t> +</templates> diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js new file mode 100644 index 00000000..cb9a3cf5 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js @@ -0,0 +1,28 @@ +odoo.define('website.s_dynamic_snippet_carousel_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); +const s_dynamic_snippet_options = require('website.s_dynamic_snippet_options'); + +const dynamicSnippetCarouselOptions = s_dynamic_snippet_options.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * + * @override + * @private + */ + _setOptionsDefaultValues: function () { + this._super.apply(this, arguments); + this._setOptionValue('carouselInterval', '5000'); + } + +}); + +options.registry.dynamic_snippet_carousel = dynamicSnippetCarouselOptions; + +return dynamicSnippetCarouselOptions; +}); diff --git a/addons/website/static/src/snippets/s_facebook_page/000.js b/addons/website/static/src/snippets/s_facebook_page/000.js new file mode 100644 index 00000000..0a88f1b3 --- /dev/null +++ b/addons/website/static/src/snippets/s_facebook_page/000.js @@ -0,0 +1,56 @@ +odoo.define('website.s_facebook_page', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var utils = require('web.utils'); + +const FacebookPageWidget = publicWidget.Widget.extend({ + selector: '.o_facebook_page', + disabledInEditableMode: false, + + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + + var params = _.pick(this.$el.data(), 'href', 'height', 'tabs', 'small_header', 'hide_cover', 'show_facepile'); + if (!params.href) { + return def; + } + params.width = utils.confine(Math.floor(this.$el.width()), 180, 500); + + var src = $.param.querystring('https://www.facebook.com/plugins/page.php', params); + this.$iframe = $('<iframe/>', { + src: src, + class: 'o_temp_auto_element', + width: params.width, + height: params.height, + css: { + border: 'none', + overflow: 'hidden', + }, + scrolling: 'no', + frameborder: '0', + allowTransparency: 'true', + }); + this.$el.append(this.$iframe); + + return def; + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + + if (this.$iframe) { + this.$iframe.remove(); + } + }, +}); + +publicWidget.registry.facebookPage = FacebookPageWidget; + +return FacebookPageWidget; +}); diff --git a/addons/website/static/src/snippets/s_facebook_page/options.js b/addons/website/static/src/snippets/s_facebook_page/options.js new file mode 100644 index 00000000..da6c94dc --- /dev/null +++ b/addons/website/static/src/snippets/s_facebook_page/options.js @@ -0,0 +1,157 @@ +odoo.define('website.s_facebook_page_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.facebookPage = options.Class.extend({ + /** + * Initializes the required facebook page data to create the iframe. + * + * @override + */ + willStart: function () { + var defs = [this._super.apply(this, arguments)]; + + var defaults = { + href: '', + height: 215, + width: 350, + tabs: '', + small_header: true, + hide_cover: true, + show_facepile: false, + }; + this.fbData = _.defaults(_.pick(this.$target.data(), _.keys(defaults)), defaults); + + if (!this.fbData.href) { + // Fetches the default url for facebook page from website config + var self = this; + defs.push(this._rpc({ + model: 'website', + method: 'search_read', + args: [[], ['social_facebook']], + limit: 1, + }).then(function (res) { + if (res) { + self.fbData.href = res[0].social_facebook || ''; + } + })); + } + + return Promise.all(defs).then(() => this._markFbElement()).then(() => this._refreshPublicWidgets()); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Toggles a checkbox option. + * + * @see this.selectClass for parameters + * @param {String} optionName the name of the option to toggle + */ + toggleOption: function (previewMode, widgetValue, params) { + let optionName = params.optionName; + if (optionName.startsWith('tab.')) { + optionName = optionName.replace('tab.', ''); + if (widgetValue) { + this.fbData.tabs = this.fbData.tabs + .split(',') + .filter(t => t !== '') + .concat([optionName]) + .join(','); + } else { + this.fbData.tabs = this.fbData.tabs + .split(',') + .filter(t => t !== optionName) + .join(','); + } + } else { + if (optionName === 'show_cover') { + this.fbData.hide_cover = !widgetValue; + } else { + this.fbData[optionName] = widgetValue; + } + } + return this._markFbElement(); + }, + /** + * Sets the facebook page's URL. + * + * @see this.selectClass for parameters + */ + pageUrl: function (previewMode, widgetValue, params) { + this.fbData.href = widgetValue; + return this._markFbElement(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Sets the correct dataAttributes on the facebook iframe and refreshes it. + * + * @see this.selectClass for parameters + */ + _markFbElement: function () { + return this._checkURL().then(() => { + // Managing height based on options + if (this.fbData.tabs) { + this.fbData.height = this.fbData.tabs === 'events' ? 300 : 500; + } else if (this.fbData.small_header) { + this.fbData.height = this.fbData.show_facepile ? 165 : 70; + } else { + this.fbData.height = this.fbData.show_facepile ? 225 : 150; + } + _.each(this.fbData, (value, key) => { + this.$target.attr('data-' + key, value); + this.$target.data(key, value); + }); + }); + }, + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + const optionName = params.optionName; + switch (methodName) { + case 'toggleOption': { + if (optionName.startsWith('tab.')) { + return this.fbData.tabs.split(',').includes(optionName.replace(/^tab./, '')); + } else { + if (optionName === 'show_cover') { + return !this.fbData.hide_cover; + } + return this.fbData[optionName]; + } + } + case 'pageUrl': { + return this._checkURL().then(() => this.fbData.href); + } + } + return this._super(...arguments); + }, + /** + * @private + */ + _checkURL: function () { + const defaultURL = 'https://www.facebook.com/Odoo'; + const match = this.fbData.href.match(/^(?:https?:\/\/)?(?:www\.)?(?:fb|facebook)\.com\/(?:([\w.]+)|[^/?#]+-([0-9]{15,16}))(?:$|[/?# ])/); + if (match) { + // Check if the page exists on Facebook or not + return new Promise((resolve, reject) => $.ajax({ + url: 'https://graph.facebook.com/' + (match[2] || match[1]) + '/picture', + success: () => resolve(), + error: () => { + this.fbData.href = defaultURL; + resolve(); + }, + })); + } + this.fbData.href = defaultURL; + return Promise.resolve(); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_faq_collapse/000.scss b/addons/website/static/src/snippets/s_faq_collapse/000.scss new file mode 100644 index 00000000..af236dea --- /dev/null +++ b/addons/website/static/src/snippets/s_faq_collapse/000.scss @@ -0,0 +1,35 @@ + +.s_faq_collapse { + .accordion .card { + .card-header { + cursor: pointer; + display: inline-block; + width: 100%; + padding: .5em 0; + border-radius: 0; + outline: none; + &:before { + content:'\f056'; + font-family: 'FontAwesome'; + display: inline-block; + margin: 0 .5em 0 .75em; + color: $gray-600; + } + &.collapsed:before { + content:'\f055'; + font-family: 'FontAwesome'; + } + &:hover, + &:focus { + text-decoration: none; + } + } + .card-body { + padding: 1em 2.25em; + } + } + .card-body p:last-child, + .card-body ul:last-child { + margin-bottom: 0; + } +} diff --git a/addons/website/static/src/snippets/s_features_grid/000.scss b/addons/website/static/src/snippets/s_features_grid/000.scss new file mode 100644 index 00000000..d66d72c5 --- /dev/null +++ b/addons/website/static/src/snippets/s_features_grid/000.scss @@ -0,0 +1,13 @@ + +.s_features_grid { + &_content { + overflow: hidden; + p { + margin-bottom: 0; + } + } + &_icon { + float: left; + margin-right: $grid-gutter-width/2; + } +} diff --git a/addons/website/static/src/snippets/s_google_map/000.js b/addons/website/static/src/snippets/s_google_map/000.js new file mode 100644 index 00000000..1fa5d3d7 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/000.js @@ -0,0 +1,96 @@ +odoo.define('website.s_google_map', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); + +publicWidget.registry.GoogleMap = publicWidget.Widget.extend({ + selector: '.s_google_map', + disabledInEditableMode: false, + + mapColors: { + lightMonoMap: [{"featureType": "administrative.locality", "elementType": "all", "stylers": [{"hue": "#2c2e33"}, {"saturation": 7}, {"lightness": 19}, {"visibility": "on"}]}, {"featureType": "landscape", "elementType": "all", "stylers": [{"hue": "#ffffff"}, {"saturation": -100}, {"lightness": 100}, {"visibility": "simplified"}]}, {"featureType": "poi", "elementType": "all", "stylers": [{"hue": "#ffffff"}, {"saturation": -100}, {"lightness": 100}, {"visibility": "off"}]}, {"featureType": "road", "elementType": "geometry", "stylers": [{"hue": "#bbc0c4"}, {"saturation": -93}, {"lightness": 31}, {"visibility": "simplified"}]}, {"featureType": "road", "elementType": "labels", "stylers": [{"hue": "#bbc0c4"}, {"saturation": -93}, {"lightness": 31}, {"visibility": "on"}]}, {"featureType": "road.arterial", "elementType": "labels", "stylers": [{"hue": "#bbc0c4"}, {"saturation": -93}, {"lightness": -2}, {"visibility": "simplified"}]}, {"featureType": "road.local", "elementType": "geometry", "stylers": [{"hue": "#e9ebed"}, {"saturation": -90}, {"lightness": -8}, {"visibility": "simplified"}]}, {"featureType": "transit", "elementType": "all", "stylers": [{"hue": "#e9ebed"}, {"saturation": 10}, {"lightness": 69}, {"visibility": "on"}]}, {"featureType": "water", "elementType": "all", "stylers": [{"hue": "#e9ebed"}, {"saturation": -78}, {"lightness": 67}, {"visibility": "simplified"}]}], + lillaMap: [{elementType: "labels", stylers: [{saturation: -20}]}, {featureType: "poi", elementType: "labels", stylers: [{visibility: "off"}]}, {featureType: 'road.highway', elementType: 'labels', stylers: [{visibility: "off"}]}, {featureType: "road.local", elementType: "labels.icon", stylers: [{visibility: "off"}]}, {featureType: "road.arterial", elementType: "labels.icon", stylers: [{visibility: "off"}]}, {featureType: "road", elementType: "geometry.stroke", stylers: [{visibility: "off"}]}, {featureType: "transit", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "poi", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "poi.government", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "poi.sport_complex", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "poi.attraction", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "poi.business", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "transit", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "transit.station", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "landscape", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "road", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "road.highway", elementType: "geometry.fill", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}, {featureType: "water", elementType: "geometry", stylers: [{hue: '#2d313f'}, {visibility: "on"}, {lightness: 5}, {saturation: -20}]}], + blueMap: [{stylers: [{hue: "#00ffe6"}, {saturation: -20}]}, {featureType: "road", elementType: "geometry", stylers: [{lightness: 100}, {visibility: "simplified"}]}, {featureType: "road", elementType: "labels", stylers: [{visibility: "off"}]}], + retroMap: [{"featureType": "administrative", "elementType": "all", "stylers": [{"visibility": "on"}, {"lightness": 33}]}, {"featureType": "landscape", "elementType": "all", "stylers": [{"color": "#f2e5d4"}]}, {"featureType": "poi.park", "elementType": "geometry", "stylers": [{"color": "#c5dac6"}]}, {"featureType": "poi.park", "elementType": "labels", "stylers": [{"visibility": "on"}, {"lightness": 20}]}, {"featureType": "road", "elementType": "all", "stylers": [{"lightness": 20}]}, {"featureType": "road.highway", "elementType": "geometry", "stylers": [{"color": "#c5c6c6"}]}, {"featureType": "road.arterial", "elementType": "geometry", "stylers": [{"color": "#e4d7c6"}]}, {"featureType": "road.local", "elementType": "geometry", "stylers": [{"color": "#fbfaf7"}]}, {"featureType": "water", "elementType": "all", "stylers": [{"visibility": "on"}, {"color": "#acbcc9"}]}], + flatMap: [{"stylers": [{"visibility": "off"}]}, {"featureType": "road", "stylers": [{"visibility": "on"}, {"color": "#ffffff"}]}, {"featureType": "road.arterial", "stylers": [{"visibility": "on"}, {"color": "#fee379"}]}, {"featureType": "road.highway", "stylers": [{"visibility": "on"}, {"color": "#fee379"}]}, {"featureType": "landscape", "stylers": [{"visibility": "on"}, {"color": "#f3f4f4"}]}, {"featureType": "water", "stylers": [{"visibility": "on"}, {"color": "#7fc8ed"}]}, {}, {"featureType": "road", "elementType": "labels", "stylers": [{"visibility": "on"}]}, {"featureType": "poi.park", "elementType": "geometry.fill", "stylers": [{"visibility": "on"}, {"color": "#83cead"}]}, {"elementType": "labels", "stylers": [{"visibility": "on"}]}, {"featureType": "landscape.man_made", "elementType": "geometry", "stylers": [{"weight": 0.9}, {"visibility": "off"}]}], + cobaltMap: [{"featureType": "all", "elementType": "all", "stylers": [{"invert_lightness": true}, {"saturation": 10}, {"lightness": 30}, {"gamma": 0.5}, {"hue": "#435158"}]}], + cupertinoMap: [{"featureType": "water", "elementType": "geometry", "stylers": [{"color": "#a2daf2"}]}, {"featureType": "landscape.man_made", "elementType": "geometry", "stylers": [{"color": "#f7f1df"}]}, {"featureType": "landscape.natural", "elementType": "geometry", "stylers": [{"color": "#d0e3b4"}]}, {"featureType": "landscape.natural.terrain", "elementType": "geometry", "stylers": [{"visibility": "off"}]}, {"featureType": "poi.park", "elementType": "geometry", "stylers": [{"color": "#bde6ab"}]}, {"featureType": "poi", "elementType": "labels", "stylers": [{"visibility": "off"}]}, {"featureType": "poi.medical", "elementType": "geometry", "stylers": [{"color": "#fbd3da"}]}, {"featureType": "poi.business", "stylers": [{"visibility": "off"}]}, {"featureType": "road", "elementType": "geometry.stroke", "stylers": [{"visibility": "off"}]}, {"featureType": "road", "elementType": "labels", "stylers": [{"visibility": "off"}]}, {"featureType": "road.highway", "elementType": "geometry.fill", "stylers": [{"color": "#ffe15f"}]}, {"featureType": "road.highway", "elementType": "geometry.stroke", "stylers": [{"color": "#efd151"}]}, {"featureType": "road.arterial", "elementType": "geometry.fill", "stylers": [{"color": "#ffffff"}]}, {"featureType": "road.local", "elementType": "geometry.fill", "stylers": [{"color": "black"}]}, {"featureType": "transit.station.airport", "elementType": "geometry.fill", "stylers": [{"color": "#cfb2db"}]}], + carMap: [{"featureType": "administrative", "stylers": [{"visibility": "off"}]}, {"featureType": "poi", "stylers": [{"visibility": "simplified"}]}, {"featureType": "road", "stylers": [{"visibility": "simplified"}]}, {"featureType": "water", "stylers": [{"visibility": "simplified"}]}, {"featureType": "transit", "stylers": [{"visibility": "simplified"}]}, {"featureType": "landscape", "stylers": [{"visibility": "simplified"}]}, {"featureType": "road.highway", "stylers": [{"visibility": "off"}]}, {"featureType": "road.local", "stylers": [{"visibility": "on"}]}, {"featureType": "road.highway", "elementType": "geometry", "stylers": [{"visibility": "on"}]}, {"featureType": "water", "stylers": [{"color": "#84afa3"}, {"lightness": 52}]}, {"stylers": [{"saturation": -77}]}, {"featureType": "road"}], + bwMap: [{stylers: [{hue: "#00ffe6"}, {saturation: -100}]}, {featureType: "road", elementType: "geometry", stylers: [{lightness: 100}, {visibility: "simplified"}]}, {featureType: "road", elementType: "labels", stylers: [{visibility: "off"}]}], + }, + + /** + * @override + */ + async start() { + await this._super(...arguments); + + if (typeof google !== 'object' || typeof google.maps !== 'object') { + await new Promise(resolve => { + this.trigger_up('gmap_api_request', { + editableMode: this.editableMode, + onSuccess: () => resolve(), + }); + }); + // The animation will be restarted for all maps as soon as the + // google map script has been executed. + return; + } + + // Define a default map's colors set + const std = []; + new google.maps.StyledMapType(std, {name: "Std Map"}); + + // Default options, will be overwritten by the user + const myOptions = { + zoom: 12, + center: new google.maps.LatLng(50.854975, 4.3753899), + mapTypeId: google.maps.MapTypeId.ROADMAP, + panControl: false, + zoomControl: false, + mapTypeControl: false, + streetViewControl: false, + scrollwheel: false, + mapTypeControlOptions: { + mapTypeIds: [google.maps.MapTypeId.ROADMAP, 'map_style'] + } + }; + + // Render Map + const mapC = this.$('.map_container'); + const map = new google.maps.Map(mapC.get(0), myOptions); + + // Update GPS position + const p = this.el.dataset.mapGps.substring(1).slice(0, -1).split(','); + const gps = new google.maps.LatLng(p[0], p[1]); + map.setCenter(gps); + + // Update Map on screen resize + google.maps.event.addDomListener(window, 'resize', () => { + map.setCenter(gps); + }); + + // Create Marker & Infowindow + const markerOptions = { + map: map, + animation: google.maps.Animation.DROP, + position: new google.maps.LatLng(p[0], p[1]) + }; + if (this.el.dataset.pinStyle === 'flat') { + markerOptions.icon = '/website/static/src/img/snippets_thumbs/s_google_map_marker.png'; + } + new google.maps.Marker(markerOptions); + + map.setMapTypeId(google.maps.MapTypeId[this.el.dataset.mapType]); // Update Map Type + map.setZoom(parseInt(this.el.dataset.mapZoom)); // Update Map Zoom + + // Update Map Color + const mapColorAttr = this.el.dataset.mapColor; + if (mapColorAttr) { + const mapColor = this.mapColors[mapColorAttr]; + map.mapTypes.set('map_style', new google.maps.StyledMapType(mapColor, {name: "Styled Map"})); + map.setMapTypeId('map_style'); + } + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_google_map/000.scss b/addons/website/static/src/snippets/s_google_map/000.scss new file mode 100644 index 00000000..9a8df732 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/000.scss @@ -0,0 +1,42 @@ + +$s-google-map-desc-bg: theme-color('primary') !default; +$s-google-map-desc-alpha: 0.80 !default; +$s-google-map-desc-hover-bg: theme-color('primary') !default; +$s-google-map-desc-hover-alpha: 0.55 !default; + +.s_google_map { + position: relative; + min-height: 100px; + + .map_container { + @include o-position-absolute(0, 0, 0, 0); + } + .description { + @include o-position-absolute(auto, 0, 0, 0); + z-index: 99; + padding: 0 1em; + background: rgba($s-google-map-desc-bg, $s-google-map-desc-alpha); + color: color-yiq(rgba($s-google-map-desc-bg, $s-google-map-desc-alpha)); + transition: background-color 250ms ease; + + font { + float: left; + margin-top: 20px; + margin-bottom: 15px; + font-weight: bold; + text-transform: uppercase; + } + span { + float: left; + text-transform: none; + font-weight: normal; + margin-top: 20px; + margin-left: 10px; + } + } + &:hover .description { + background: $s-google-map-desc-hover-bg; + background: rgba($s-google-map-desc-hover-bg, $s-google-map-desc-hover-alpha); + color: color-yiq(rgba($s-google-map-desc-hover-bg, $s-google-map-desc-hover-alpha)); + } +} diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-blue.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-blue.jpg Binary files differnew file mode 100644 index 00000000..a929e345 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-blue.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-bw.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-bw.jpg Binary files differnew file mode 100644 index 00000000..1dad96b4 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-bw.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-caramello.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-caramello.jpg Binary files differnew file mode 100644 index 00000000..55f1046c --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-caramello.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-cobalt.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-cobalt.jpg Binary files differnew file mode 100644 index 00000000..dc0aa590 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-cobalt.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-cupertino.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-cupertino.jpg Binary files differnew file mode 100644 index 00000000..06c97ae0 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-cupertino.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-default.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-default.jpg Binary files differnew file mode 100644 index 00000000..ea5c8360 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-default.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-flat.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-flat.jpg Binary files differnew file mode 100644 index 00000000..9b4df178 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-flat.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-lightMono.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-lightMono.jpg Binary files differnew file mode 100644 index 00000000..17e23249 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-lightMono.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-lilla.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-lilla.jpg Binary files differnew file mode 100644 index 00000000..62a19e7c --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-lilla.jpg diff --git a/addons/website/static/src/snippets/s_google_map/img/thumbs/map-retro.jpg b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-retro.jpg Binary files differnew file mode 100644 index 00000000..2c36791b --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/img/thumbs/map-retro.jpg diff --git a/addons/website/static/src/snippets/s_google_map/options.js b/addons/website/static/src/snippets/s_google_map/options.js new file mode 100644 index 00000000..6aad46f1 --- /dev/null +++ b/addons/website/static/src/snippets/s_google_map/options.js @@ -0,0 +1,56 @@ +odoo.define('options.s_google_map_options', function (require) { +'use strict'; + +const {_t} = require('web.core'); +const options = require('web_editor.snippets.options'); + +options.registry.GoogleMap = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * @see this.selectClass for parameters + */ + resetMapColor(previewMode, widgetValue, params) { + this.$target[0].dataset.mapColor = ''; + }, + /** + * @see this.selectClass for parameters + */ + setFormattedAddress(previewMode, widgetValue, params) { + this.$target[0].dataset.pinAddress = params.gmapPlace.formatted_address; + }, + /** + * @see this.selectClass for parameters + */ + async showDescription(previewMode, widgetValue, params) { + const descriptionEl = this.$target[0].querySelector('.description'); + if (widgetValue && !descriptionEl) { + this.$target.append($(` + <div class="description"> + <font>${_t('Visit us:')}</font> + <span>${_t('Our office is located in the northeast of Brussels. TEL (555) 432 2365')}</span> + </div>`) + ); + } else if (!widgetValue && descriptionEl) { + descriptionEl.remove(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState(methodName, params) { + if (methodName === 'showDescription') { + return this.$target[0].querySelector('.description') ? 'true' : ''; + } + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_hr/000.scss b/addons/website/static/src/snippets/s_hr/000.scss new file mode 100644 index 00000000..410000e2 --- /dev/null +++ b/addons/website/static/src/snippets/s_hr/000.scss @@ -0,0 +1,11 @@ + +.s_hr { + line-height: 0; + hr { + padding: 0; + border: 0; + border-top: 1px solid currentColor; + margin: 0; + color: inherit; + } +} diff --git a/addons/website/static/src/snippets/s_image_gallery/000.js b/addons/website/static/src/snippets/s_image_gallery/000.js new file mode 100644 index 00000000..9bd0f8e3 --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/000.js @@ -0,0 +1,180 @@ +odoo.define('website.s_image_gallery', function (require) { +'use strict'; + +var core = require('web.core'); +var publicWidget = require('web.public.widget'); + +var qweb = core.qweb; + +const GalleryWidget = publicWidget.Widget.extend({ + + selector: '.s_image_gallery:not(.o_slideshow)', + xmlDependencies: ['/website/static/src/snippets/s_image_gallery/000.xml'], + events: { + 'click img': '_onClickImg', + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when an image is clicked. Opens a dialog to browse all the images + * with a bigger size. + * + * @private + * @param {Event} ev + */ + _onClickImg: function (ev) { + var self = this; + var $cur = $(ev.currentTarget); + + var $images = $cur.closest('.s_image_gallery').find('img'); + var size = 0.8; + var dimensions = { + min_width: Math.round(window.innerWidth * size * 0.9), + min_height: Math.round(window.innerHeight * size), + max_width: Math.round(window.innerWidth * size * 0.9), + max_height: Math.round(window.innerHeight * size), + width: Math.round(window.innerWidth * size * 0.9), + height: Math.round(window.innerHeight * size) + }; + + var $img = ($cur.is('img') === true) ? $cur : $cur.closest('img'); + + const milliseconds = $cur.closest('.s_image_gallery').data('interval') || false; + var $modal = $(qweb.render('website.gallery.slideshow.lightbox', { + images: $images.get(), + index: $images.index($img), + dim: dimensions, + interval: milliseconds || 0, + id: _.uniqueId('slideshow_'), + })); + $modal.modal({ + keyboard: true, + backdrop: true, + }); + $modal.on('hidden.bs.modal', function () { + $(this).hide(); + $(this).siblings().filter('.modal-backdrop').remove(); // bootstrap leaves a modal-backdrop + $(this).remove(); + }); + $modal.find('.modal-content, .modal-body.o_slideshow').css('height', '100%'); + $modal.appendTo(document.body); + + $modal.one('shown.bs.modal', function () { + self.trigger_up('widgets_start_request', { + editableMode: false, + $target: $modal.find('.modal-body.o_slideshow'), + }); + }); + }, +}); + +const GallerySliderWidget = publicWidget.Widget.extend({ + selector: '.o_slideshow', + xmlDependencies: ['/website/static/src/snippets/s_image_gallery/000.xml'], + disabledInEditableMode: false, + + /** + * @override + */ + start: function () { + var self = this; + this.$carousel = this.$target.is('.carousel') ? this.$target : this.$target.find('.carousel'); + this.$indicator = this.$carousel.find('.carousel-indicators'); + this.$prev = this.$indicator.find('li.o_indicators_left').css('visibility', ''); // force visibility as some databases have it hidden + this.$next = this.$indicator.find('li.o_indicators_right').css('visibility', ''); + var $lis = this.$indicator.find('li[data-slide-to]'); + let indicatorWidth = this.$indicator.width(); + if (indicatorWidth === 0) { + // An ancestor may be hidden so we try to find it and make it + // visible just to take the correct width. + const $indicatorParent = this.$indicator.parents().not(':visible').last(); + if (!$indicatorParent[0].style.display) { + $indicatorParent[0].style.display = 'block'; + indicatorWidth = this.$indicator.width(); + $indicatorParent[0].style.display = ''; + } + } + let nbPerPage = Math.floor(indicatorWidth / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space + var realNbPerPage = nbPerPage || 1; + var nbPages = Math.ceil($lis.length / realNbPerPage); + + var index; + var page; + update(); + + function hide() { + $lis.each(function (i) { + $(this).toggleClass('d-none', i < page * nbPerPage || i >= (page + 1) * nbPerPage); + }); + if (page <= 0) { + self.$prev.detach(); + } else { + self.$prev.removeClass('d-none'); + self.$prev.prependTo(self.$indicator); + } + if (page >= nbPages - 1) { + self.$next.detach(); + } else { + self.$next.removeClass('d-none'); + self.$next.appendTo(self.$indicator); + } + } + + function update() { + const active = $lis.filter('.active'); + index = active.length ? $lis.index(active) : 0; + page = Math.floor(index / realNbPerPage); + hide(); + } + + this.$carousel.on('slide.bs.carousel.gallery_slider', function () { + setTimeout(function () { + var $item = self.$carousel.find('.carousel-inner .carousel-item-prev, .carousel-inner .carousel-item-next'); + var index = $item.index(); + $lis.removeClass('active') + .filter('[data-slide-to="' + index + '"]') + .addClass('active'); + }, 0); + }); + this.$indicator.on('click.gallery_slider', '> li:not([data-slide-to])', function () { + page += ($(this).hasClass('o_indicators_left') ? -1 : 1); + page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary + self.$carousel.carousel(page * realNbPerPage); + // We dont use hide() before the slide animation in the editor because there is a traceback + // TO DO: fix this traceback + if (!self.editableMode) { + hide(); + } + }); + this.$carousel.on('slid.bs.carousel.gallery_slider', update); + + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + + if (!this.$indicator) { + return; + } + + this.$prev.prependTo(this.$indicator); + this.$next.appendTo(this.$indicator); + this.$carousel.off('.gallery_slider'); + this.$indicator.off('.gallery_slider'); + }, +}); + +publicWidget.registry.gallery = GalleryWidget; +publicWidget.registry.gallerySlider = GallerySliderWidget; + +return { + GalleryWidget: GalleryWidget, + GallerySliderWidget: GallerySliderWidget, +}; +}); diff --git a/addons/website/static/src/snippets/s_image_gallery/000.scss b/addons/website/static/src/snippets/s_image_gallery/000.scss new file mode 100644 index 00000000..a99bc86f --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/000.scss @@ -0,0 +1,155 @@ + +.o_gallery:not([data-vcss]) { + &.o_grid, &.o_masonry { + .img { + width: 100%; + } + } + &.o_grid { + &.o_spc-none div.row { + margin: 0; + > div { + padding: 0; + } + } + &.o_spc-small div.row { + margin: 5px 0; + > div { + padding: 0 5px; + } + } + &.o_spc-medium div.row { + margin: 10px 0; + > div { + padding: 0 10px; + } + } + &.o_spc-big div.row { + margin: 15px 0; + > div { + padding: 0 15px; + } + } + &.size-auto .row { + height: auto; + } + &.size-small .row { + height: 100px; + } + &.size-medium .row { + height: 250px; + } + &.size-big .row { + height: 400px; + } + &.size-small, &.size-medium, &.size-big { + img { + height: 100%; + } + } + } + &.o_masonry { + &.o_spc-none div.col { + padding: 0; + > img { + margin: 0 !important; + } + } + &.o_spc-small div.col { + padding: 0 5px; + > img { + margin: 5px 0 !important; + } + } + &.o_spc-medium div.col { + padding: 0 10px; + > img { + margin: 10px 0 !important; + } + } + &.o_spc-big div.col { + padding: 0 15px; + > img { + margin: 15px 0 !important; + } + } + } + &.o_nomode { + &.o_spc-none .img { + padding: 0; + } + &.o_spc-small .img { + padding: 5px; + } + &.o_spc-medium .img { + padding: 10px; + } + &.o_spc-big .img { + padding: 15px; + } + } + &.o_slideshow { + .carousel ul.carousel-indicators li { + border: 1px solid #aaa; + } + > div:first-child { + height: 100%; + } + .carousel { + height: 100%; + + .carousel-inner { + height: 100%; + } + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev { + display: flex; + align-items: center; + height: 100%; + padding-bottom: 64px; + } + img { + max-height: 100%; + max-width: 100%; + margin: auto; + } + ul.carousel-indicators { + height: auto; + padding: 0; + border-width: 0; + position: absolute; + bottom: 0; + width: 100%; + margin-left: 0; + left: 0%; + > * { + list-style-image: none; + display: inline-block; + width: 40px; + height: 40px; + line-height: 40px; + margin: 2.5px 2.5px 2.5px 2.5px; + padding: 0 !important; + border: 1px solid #aaa; + text-indent: initial; + background-size: cover; + background-color: #fff; + border-radius: 0; + vertical-align: bottom; + flex: 0 0 40px; + &:not(.active) { + opacity: 0.8; + filter: grayscale(1); + } + } + } + } + } + .carousel-inner .item img { + max-width: none; + } +} + +// Note: the s_gallery_lightbox is always using the right dom and classes of the +// most recent version of the snippet as it is generated by JS after page load. diff --git a/addons/website/static/src/snippets/s_image_gallery/000.xml b/addons/website/static/src/snippets/s_image_gallery/000.xml new file mode 100644 index 00000000..a50a1af5 --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/000.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <!-- + ======================================================================== + Gallery Slideshow + + This template is used to display a slideshow of images inside a + bootstrap carousel. + + ======================================================================== + --> + <t t-name="website.gallery.slideshow"> + <div t-attf-id="#{id}" class="carousel slide" data-ride="carousel" t-attf-data-interval="#{interval}" style="margin: 0 12px;"> + <div class="carousel-inner" style="padding: 0;"> + <t t-foreach="images" t-as="image"> + <div t-attf-class="carousel-item #{image_index == index and 'active' or None}"> + <img t-attf-class="#{attrClass || 'img img-fluid d-block'}" t-att-src="image.src" t-att-style="attrStyle" t-att-alt="image.alt" data-name="Image"/> + </div> + </t> + </div> + + <ul class="carousel-indicators"> + <li class="o_indicators_left text-center d-none" aria-label="Previous" title="Previous"> + <i class="fa fa-chevron-left"/> + </li> + <t t-foreach="images" t-as="image"> + <li t-attf-data-target="##{id}" t-att-data-slide-to="image_index" t-att-class="image_index == index and 'active' or None" t-attf-style="background-image: url(#{image.src})"></li> + </t> + <li class="o_indicators_right text-center d-none" aria-label="Next" title="Next"> + <i class="fa fa-chevron-right"/> + </li> + </ul> + + <a class="carousel-control-prev o_we_no_overlay" t-attf-href="##{id}" data-slide="prev" aria-label="Previous" title="Previous"> + <span class="fa fa-chevron-left fa-2x text-white"></span> + <span class="sr-only">Previous</span> + </a> + <a class="carousel-control-next o_we_no_overlay" t-attf-href="##{id}" data-slide="next" aria-label="Next" title="Next"> + <span class="fa fa-chevron-right fa-2x text-white"></span> + <span class="sr-only">Next</span> + </a> + </div> + </t> + + <!-- + ======================================================================== + Gallery Slideshow LightBox + + This template is used to display a lightbox with a slideshow. + + This template wraps website.gallery.slideshow in a bootstrap modal + dialog. + ======================================================================== + --> + <t t-name="website.gallery.slideshow.lightbox"> + <div role="dialog" class="modal o_technical_modal fade s_gallery_lightbox p-0" aria-labbelledby="Image Gallery Dialog"> + <div class="modal-dialog m-0" role="Picture Gallery" + t-attf-style=""> + <div class="modal-content bg-transparent"> + <main class="modal-body o_slideshow bg-transparent"> + <button type="button" class="close text-white" data-dismiss="modal" style="position: absolute; right: 10px; top: 10px;"><span role="img" aria-label="Close">×</span><span class="sr-only">Close</span></button> + <t t-call="website.gallery.slideshow"></t> + </main> + </div> + </div> + </div> + </t> +</templates> diff --git a/addons/website/static/src/snippets/s_image_gallery/001.scss b/addons/website/static/src/snippets/s_image_gallery/001.scss new file mode 100644 index 00000000..b20d0be3 --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/001.scss @@ -0,0 +1,281 @@ + +.s_image_gallery[data-vcss="001"] { + &.o_grid, &.o_masonry { + .img { + width: 100%; + } + } + &.o_grid { + &.o_spc-none div.row { + margin-bottom: 0px; + } + &.o_spc-small div.row > div { + margin-bottom: $spacer; + } + &.o_spc-medium div.row > div { + margin-bottom: $spacer * 2; + } + &.o_spc-big div.row > div { + margin-bottom: $spacer * 3; + } + } + &.o_masonry { + &.o_spc-none div.o_masonry_col { + padding: 0; + > img { + margin: 0 !important; + } + } + &.o_spc-small div.o_masonry_col { + padding: 0 ($spacer * .5); + > img { + margin-bottom: $spacer !important; + } + } + &.o_spc-medium div.o_masonry_col { + padding: 0 $spacer; + > img { + margin-bottom: $spacer * 2 !important; + } + } + &.o_spc-big div.o_masonry_col { + padding: 0 ($spacer * 1.5); + > img { + margin-bottom: $spacer * 3 !important; + } + } + } + &.o_nomode { + &.o_spc-none .row div { + padding-top: 0; + padding-bottom: 0; + } + &.o_spc-small .row div { + padding-top: $spacer * .5; + padding-bottom: $spacer * .5; + } + &.o_spc-medium .row div { + padding-top: $spacer; + padding-bottom: $spacer; + } + &.o_spc-big .row div { + padding-top: $spacer * 1.5; + padding-bottom: $spacer * 1.5; + } + } + &:not(.o_slideshow) { + img { + cursor: pointer; + } + } + &.o_slideshow { + .carousel { + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev, + .carousel-control-next, + .carousel-control-prev { + padding-bottom: 64px; + } + ul.carousel-indicators li { + border: 1px solid #aaa; + } + } + ul.carousel-indicators { + position: absolute; + left: 0%; + bottom: 0; + width: 100%; + height: auto; + margin-left: 0; + padding: 0; + border-width: 0; + > * { + list-style-image: none; + display: inline-block; + width: 40px; + height: 40px; + line-height: 40px; + margin: 2.5px 2.5px 2.5px 2.5px; + padding: 0; + border: 1px solid #aaa; + text-indent: initial; + background-size: cover; + background-color: #fff; + background-position: center; + border-radius: 0; + vertical-align: bottom; + flex: 0 0 40px; + &:not(.active) { + opacity: 0.8; + filter: grayscale(1); + } + } + } + > .container, > .container-fluid, > .o_container_small { + height: 100%; + } + &.s_image_gallery_cover .carousel-item { + > a { + width: 100%; + height: 100%; + } + > a > img, + > img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + &:not(.s_image_gallery_show_indicators) .carousel { + ul.carousel-indicators { + display: none; + } + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev, + .carousel-control-next, + .carousel-control-prev { + padding-bottom: 0px; + } + } + &.s_image_gallery_indicators_arrows_boxed, &.s_image_gallery_indicators_arrows_rounded { + .carousel { + .carousel-control-prev .fa, + .carousel-control-next .fa { + text-shadow: none; + } + } + } + &.s_image_gallery_indicators_arrows_boxed { + .carousel { + .carousel-control-prev .fa:before { + content: "\f104"; + padding-right: 2px; + } + .carousel-control-next .fa:before { + content: "\f105"; + padding-left: 2px; + } + .carousel-control-prev .fa:before, + .carousel-control-next .fa:before { + display: block; + width: 3rem; + height: 3rem; + line-height: 3rem; + color: black; + background: white; + font-size: 1.25rem; + border: 1px solid $gray-500; + } + } + } + &.s_image_gallery_indicators_arrows_rounded { + .carousel { + .carousel-control-prev .fa:before { content: "\f060"; } + .carousel-control-next .fa:before { content: "\f061"; } + .carousel-control-prev .fa:before, + .carousel-control-next .fa:before { + color: black; + background: white; + font-size: 1.25rem; + border-radius: 50%; + padding: 1.25rem; + border: 1px solid $gray-500; + } + } + } + &.s_image_gallery_indicators_rounded { + .carousel { + ul.carousel-indicators li { + border-radius: 50%; + } + } + } + &.s_image_gallery_indicators_dots { + .carousel { + ul.carousel-indicators { + height: 40px; + margin: auto; + + li { + max-width: 8px; + max-height: 8px; + margin: 0 6px; + border-radius: 10px; + background-color: $black; + background-image: none !important; + + &:not(.active) { + opacity: .4; + } + } + } + } + } + + @extend %image-gallery-slideshow-styles; + } + .carousel-inner .item img { + max-width: none; + } +} + +.s_gallery_lightbox { + .close { + font-size: 2rem; + } + .modal-dialog { + height: 100%; + background-color: rgba(0,0,0,0.7); + } + @include media-breakpoint-up(sm) { + .modal-dialog { + max-width: 100%; + padding: 0; + } + } + ul.carousel-indicators { + display: none; + } + + .modal-body.o_slideshow { + @extend %image-gallery-slideshow-styles; + } +} + +%image-gallery-slideshow-styles { + &:not(.s_image_gallery_cover) .carousel-item { + > a { + display: flex; + height: 100%; + width: 100%; + } + > a > img, + > img { + max-height: 100%; + max-width: 100%; + margin: auto; + } + } + .carousel { + height: 100%; + + .carousel-inner { + height: 100%; + } + .carousel-item.active, + .carousel-item-next, + .carousel-item-prev, + .carousel-control-next, + .carousel-control-prev { + display: flex; + align-items: center; + height: 100%; + } + .carousel-control-next .fa, + .carousel-control-prev .fa { + text-shadow: 0px 0px 3px $gray-800; + } + } +} diff --git a/addons/website/static/src/snippets/s_image_gallery/options.js b/addons/website/static/src/snippets/s_image_gallery/options.js new file mode 100644 index 00000000..8afb5c1f --- /dev/null +++ b/addons/website/static/src/snippets/s_image_gallery/options.js @@ -0,0 +1,481 @@ +odoo.define('website.s_image_gallery_options', function (require) { +'use strict'; + +var core = require('web.core'); +var weWidgets = require('wysiwyg.widgets'); +var options = require('web_editor.snippets.options'); + +var _t = core._t; +var qweb = core.qweb; + +options.registry.gallery = options.Class.extend({ + xmlDependencies: ['/website/static/src/snippets/s_image_gallery/000.xml'], + + /** + * @override + */ + start: function () { + var self = this; + + // Make sure image previews are updated if images are changed + this.$target.on('image_changed', 'img', function (ev) { + var $img = $(ev.currentTarget); + var index = self.$target.find('.carousel-item.active').index(); + self.$('.carousel:first li[data-target]:eq(' + index + ')') + .css('background-image', 'url(' + $img.attr('src') + ')'); + }); + + // When the snippet is empty, an edition button is the default content + // TODO find a nicer way to do that to have editor style + this.$target.on('click', '.o_add_images', function (e) { + e.stopImmediatePropagation(); + self.addImages(false); + }); + + this.$target.on('dropped', 'img', function (ev) { + self.mode(null, self.getMode()); + if (!ev.target.height) { + $(ev.target).one('load', function () { + setTimeout(function () { + self.trigger_up('cover_update'); + }); + }); + } + }); + + const $container = this.$('> .container, > .container-fluid, > .o_container_small'); + if ($container.find('> *:not(div)').length) { + self.mode(null, self.getMode()); + } + + return this._super.apply(this, arguments); + }, + /** + * @override + */ + onBuilt: function () { + if (this.$target.find('.o_add_images').length) { + this.addImages(false); + } + // TODO should consider the async parts + this._adaptNavigationIDs(); + }, + /** + * @override + */ + onClone: function () { + this._adaptNavigationIDs(); + }, + /** + * @override + */ + cleanForSave: function () { + if (this.$target.hasClass('slideshow')) { + this.$target.removeAttr('style'); + } + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Allows to select images to add as part of the snippet. + * + * @see this.selectClass for parameters + */ + addImages: function (previewMode) { + const $images = this.$('img'); + var $container = this.$('> .container, > .container-fluid, > .o_container_small'); + var dialog = new weWidgets.MediaDialog(this, {multiImages: true, onlyImages: true, mediaWidth: 1920}); + var lastImage = _.last(this._getImages()); + var index = lastImage ? this._getIndex(lastImage) : -1; + return new Promise(resolve => { + dialog.on('save', this, function (attachments) { + for (var i = 0; i < attachments.length; i++) { + $('<img/>', { + class: $images.length > 0 ? $images[0].className : 'img img-fluid d-block ', + src: attachments[i].image_src, + 'data-index': ++index, + alt: attachments[i].description || '', + 'data-name': _t('Image'), + style: $images.length > 0 ? $images[0].style.cssText : '', + }).appendTo($container); + } + if (attachments.length > 0) { + this.mode('reset', this.getMode()); + this.trigger_up('cover_update'); + } + }); + dialog.on('closed', this, () => resolve()); + dialog.open(); + }); + }, + /** + * Allows to change the number of columns when displaying images with a + * grid-like layout. + * + * @see this.selectClass for parameters + */ + columns: function (previewMode, widgetValue, params) { + const nbColumns = parseInt(widgetValue || '1'); + this.$target.attr('data-columns', nbColumns); + + this.mode(previewMode, this.getMode(), {}); // TODO improve + }, + /** + * Get the image target's layout mode (slideshow, masonry, grid or nomode). + * + * @returns {String('slideshow'|'masonry'|'grid'|'nomode')} + */ + getMode: function () { + var mode = 'slideshow'; + if (this.$target.hasClass('o_masonry')) { + mode = 'masonry'; + } + if (this.$target.hasClass('o_grid')) { + mode = 'grid'; + } + if (this.$target.hasClass('o_nomode')) { + mode = 'nomode'; + } + return mode; + }, + /** + * Displays the images with the "grid" layout. + */ + grid: function () { + var imgs = this._getImages(); + var $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + var columns = this._getColumns(); + var colClass = 'col-lg-' + (12 / columns); + var $container = this._replaceContent($row); + + _.each(imgs, function (img, index) { + var $img = $(img); + var $col = $('<div/>', {class: colClass}); + $col.append($img).appendTo($row); + if ((index + 1) % columns === 0) { + $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + $row.appendTo($container); + } + }); + this.$target.css('height', ''); + }, + /** + * Displays the images with the "masonry" layout. + */ + masonry: function () { + var self = this; + var imgs = this._getImages(); + var columns = this._getColumns(); + var colClass = 'col-lg-' + (12 / columns); + var cols = []; + + var $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + this._replaceContent($row); + + // Create columns + for (var c = 0; c < columns; c++) { + var $col = $('<div/>', {class: 'o_masonry_col o_snippet_not_selectable ' + colClass}); + $row.append($col); + cols.push($col[0]); + } + + // Dispatch images in columns by always putting the next one in the + // smallest-height column + while (imgs.length) { + var min = Infinity; + var $lowest; + _.each(cols, function (col) { + var $col = $(col); + var height = $col.is(':empty') ? 0 : $col.find('img').last().offset().top + $col.find('img').last().height() - self.$target.offset().top; + if (height < min) { + min = height; + $lowest = $col; + } + }); + $lowest.append(imgs.shift()); + } + }, + /** + * Allows to change the images layout. @see grid, masonry, nomode, slideshow + * + * @see this.selectClass for parameters + */ + mode: function (previewMode, widgetValue, params) { + widgetValue = widgetValue || 'slideshow'; // FIXME should not be needed + this.$target.css('height', ''); + this.$target + .removeClass('o_nomode o_masonry o_grid o_slideshow') + .addClass('o_' + widgetValue); + this[widgetValue](); + this.trigger_up('cover_update'); + this._refreshPublicWidgets(); + }, + /** + * Displays the images with the standard layout: floating images. + */ + nomode: function () { + var $row = $('<div/>', {class: 'row s_nb_column_fixed'}); + var imgs = this._getImages(); + + this._replaceContent($row); + + _.each(imgs, function (img) { + var wrapClass = 'col-lg-3'; + if (img.width >= img.height * 2 || img.width > 600) { + wrapClass = 'col-lg-6'; + } + var $wrap = $('<div/>', {class: wrapClass}).append(img); + $row.append($wrap); + }); + }, + /** + * Allows to remove all images. Restores the snippet to the way it was when + * it was added in the page. + * + * @see this.selectClass for parameters + */ + removeAllImages: function (previewMode) { + var $addImg = $('<div>', { + class: 'alert alert-info css_non_editable_mode_hidden text-center', + }); + var $text = $('<span>', { + class: 'o_add_images', + style: 'cursor: pointer;', + text: _t(" Add Images"), + }); + var $icon = $('<i>', { + class: ' fa fa-plus-circle', + }); + this._replaceContent($addImg.append($icon).append($text)); + }, + /** + * Displays the images with a "slideshow" layout. + */ + slideshow: function () { + const imageEls = this._getImages(); + const images = _.map(imageEls, img => ({ + // Use getAttribute to get the attribute value otherwise .src + // returns the absolute url. + src: img.getAttribute('src'), + alt: img.getAttribute('alt'), + })); + var currentInterval = this.$target.find('.carousel:first').attr('data-interval'); + var params = { + images: images, + index: 0, + title: "", + interval: currentInterval || 0, + id: 'slideshow_' + new Date().getTime(), + attrClass: imageEls.length > 0 ? imageEls[0].className : '', + attrStyle: imageEls.length > 0 ? imageEls[0].style.cssText : '', + }, + $slideshow = $(qweb.render('website.gallery.slideshow', params)); + this._replaceContent($slideshow); + _.each(this.$('img'), function (img, index) { + $(img).attr({contenteditable: true, 'data-index': index}); + }); + this.$target.css('height', Math.round(window.innerHeight * 0.7)); + + // Apply layout animation + this.$target.off('slide.bs.carousel').off('slid.bs.carousel'); + this.$('li.fa').off('click'); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Handles image removals and image index updates. + * + * @override + */ + notify: function (name, data) { + this._super(...arguments); + if (name === 'image_removed') { + data.$image.remove(); // Force the removal of the image before reset + this.mode('reset', this.getMode()); + } else if (name === 'image_index_request') { + var imgs = this._getImages(); + var position = _.indexOf(imgs, data.$image[0]); + imgs.splice(position, 1); + switch (data.position) { + case 'first': + imgs.unshift(data.$image[0]); + break; + case 'prev': + imgs.splice(position - 1, 0, data.$image[0]); + break; + case 'next': + imgs.splice(position + 1, 0, data.$image[0]); + break; + case 'last': + imgs.push(data.$image[0]); + break; + } + position = imgs.indexOf(data.$image[0]); + _.each(imgs, function (img, index) { + // Note: there might be more efficient ways to do that but it is + // more simple this way and allows compatibility with 10.0 where + // indexes were not the same as positions. + $(img).attr('data-index', index); + }); + const currentMode = this.getMode(); + this.mode('reset', currentMode); + if (currentMode === 'slideshow') { + const $carousel = this.$target.find('.carousel'); + $carousel.removeClass('slide'); + $carousel.carousel(position); + this.$target.find('.carousel-indicators li').removeClass('active'); + this.$target.find('.carousel-indicators li[data-slide-to="' + position + '"]').addClass('active'); + this.trigger_up('activate_snippet', { + $snippet: this.$target.find('.carousel-item.active img'), + ifInactiveOptions: true, + }); + $carousel.addClass('slide'); + } else { + this.trigger_up('activate_snippet', { + $snippet: data.$image, + ifInactiveOptions: true, + }); + } + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _adaptNavigationIDs: function () { + var uuid = new Date().getTime(); + this.$target.find('.carousel').attr('id', 'slideshow_' + uuid); + _.each(this.$target.find('[data-slide], [data-slide-to]'), function (el) { + var $el = $(el); + if ($el.attr('data-target')) { + $el.attr('data-target', '#slideshow_' + uuid); + } else if ($el.attr('href')) { + $el.attr('href', '#slideshow_' + uuid); + } + }); + }, + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'mode': { + let activeModeName = 'slideshow'; + for (const modeName of params.possibleValues) { + if (this.$target.hasClass(`o_${modeName}`)) { + activeModeName = modeName; + break; + } + } + this.activeMode = activeModeName; + return activeModeName; + } + case 'columns': { + return `${this._getColumns()}`; + } + } + return this._super(...arguments); + }, + /** + * @private + */ + async _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'slideshow_mode_opt') { + return false; + } + return this._super(...arguments); + }, + /** + * Returns the images, sorted by index. + * + * @private + * @returns {DOMElement[]} + */ + _getImages: function () { + var imgs = this.$('img').get(); + var self = this; + imgs.sort(function (a, b) { + return self._getIndex(a) - self._getIndex(b); + }); + return imgs; + }, + /** + * Returns the index associated to a given image. + * + * @private + * @param {DOMElement} img + * @returns {integer} + */ + _getIndex: function (img) { + return img.dataset.index || 0; + }, + /** + * Returns the currently selected column option. + * + * @private + * @returns {integer} + */ + _getColumns: function () { + return parseInt(this.$target.attr('data-columns')) || 3; + }, + /** + * Empties the container, adds the given content and returns the container. + * + * @private + * @param {jQuery} $content + * @returns {jQuery} the main container of the snippet + */ + _replaceContent: function ($content) { + var $container = this.$('> .container, > .container-fluid, > .o_container_small'); + $container.empty().append($content); + return $container; + }, +}); + +options.registry.gallery_img = options.Class.extend({ + /** + * Rebuilds the whole gallery when one image is removed. + * + * @override + */ + onRemove: function () { + this.trigger_up('option_update', { + optionName: 'gallery', + name: 'image_removed', + data: { + $image: this.$target, + }, + }); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Allows to change the position of an image (its order in the image set). + * + * @see this.selectClass for parameters + */ + position: function (previewMode, widgetValue, params) { + this.trigger_up('option_update', { + optionName: 'gallery', + name: 'image_index_request', + data: { + $image: this.$target, + position: widgetValue, + }, + }); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_masonry_block/000.scss b/addons/website/static/src/snippets/s_masonry_block/000.scss new file mode 100644 index 00000000..19e10320 --- /dev/null +++ b/addons/website/static/src/snippets/s_masonry_block/000.scss @@ -0,0 +1,93 @@ +.s_masonry_block:not([data-vcss]) { + .row, .row > div { + display: flex; + } + + .row > div { + padding-bottom: $grid-gutter-width/2; + padding-top: $grid-gutter-width/2; + justify-content: center; + flex-flow: column wrap; + } + + .container-fluid > .row { + flex-flow: row nowrap; + + @include media-breakpoint-down(md) { + flex-flow: column nowrap; + } + + > div.s_masonry_block_pseudo_col { + flex: 1 1 auto; + padding:0; + + > .row { + flex-flow: row wrap; + min-height: 100%; + margin: 0; + + @include media-breakpoint-down(sm) { + flex-flow: column nowrap; + } + + > div { + min-height: 50%; + flex: 1 1 auto; + } + } + } + } + + &.s_ratio_2_1 { + .row > div { + padding-top: $grid-gutter-width; + padding-bottom: $grid-gutter-width; + } + } +} + +html[data-no-flex] .s_masonry_block:not([data-vcss]) { + min-height: 340px; + > div { + height: 100%; + } + + .row { + height: 100%; + + > div { + position: relative; + height: 100%; + min-height: 170px; + padding-top: 0; + padding-left: 0; + } + } + + .content { + @include clearfix; + } + + @include media-breakpoint-up(md) { + .row .row > div { + height: 50%; + } + } + + @include media-breakpoint-up(lg) { + height: 0px; // hack to force height chain + &.s_ratio_2_1 { + position: relative; + padding: 0 0 50% 0; // to have 2:1 aspect ratio + > div { + padding-top: 0; + padding-bottom:0; + @include o-position-absolute(0, 0, 0, 0); + } + } + + .content { + @include o-position-absolute($s-masonry-block-content-top, $s-masonry-block-content-right, $s-masonry-block-content-bottom, $s-masonry-block-content-left); + } + } +} diff --git a/addons/website/static/src/snippets/s_masonry_block/000_variables.scss b/addons/website/static/src/snippets/s_masonry_block/000_variables.scss new file mode 100644 index 00000000..2261e63a --- /dev/null +++ b/addons/website/static/src/snippets/s_masonry_block/000_variables.scss @@ -0,0 +1,4 @@ +$s-masonry-block-content-top: 35%; +$s-masonry-block-content-right: 25%; +$s-masonry-block-content-bottom: 35%; +$s-masonry-block-content-left: 25%; diff --git a/addons/website/static/src/snippets/s_masonry_block/001.scss b/addons/website/static/src/snippets/s_masonry_block/001.scss new file mode 100644 index 00000000..f5020295 --- /dev/null +++ b/addons/website/static/src/snippets/s_masonry_block/001.scss @@ -0,0 +1,5 @@ +.s_masonry_block[data-vcss='001'] .row > div { + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/addons/website/static/src/snippets/s_media_list/000.scss b/addons/website/static/src/snippets/s_media_list/000.scss new file mode 100644 index 00000000..6c22e352 --- /dev/null +++ b/addons/website/static/src/snippets/s_media_list/000.scss @@ -0,0 +1,27 @@ + +.s_media_list:not([data-vcss]) { + .row { + margin: $grid-gutter-width 0; + } + [class*="col-"] { + padding: 0; + } + .s_media_list_body { + padding: $grid-gutter-width; + background-color: gray('white'); + } + .s_media_list_options { + @include o-position-absolute(auto,0,0,0); + display: flex; + border-top: 1px solid gray('400'); + .s_media_list_option { + flex: 1 1 auto; + padding: $grid-gutter-width/3 0; + border-right: 1px solid gray('400'); + text-align: center; + &:last-child { + border-right: 0; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_media_list/001.scss b/addons/website/static/src/snippets/s_media_list/001.scss new file mode 100644 index 00000000..aae95e2c --- /dev/null +++ b/addons/website/static/src/snippets/s_media_list/001.scss @@ -0,0 +1,11 @@ +.s_media_list[data-vcss="001"] { + .s_media_list_item > .row { + overflow: hidden; // To support rounded option + } + .s_media_list_body { + padding: $spacer * 2; + } + .s_media_list_img { + object-fit: cover; + } +} diff --git a/addons/website/static/src/snippets/s_media_list/options.js b/addons/website/static/src/snippets/s_media_list/options.js new file mode 100644 index 00000000..ee277fcf --- /dev/null +++ b/addons/website/static/src/snippets/s_media_list/options.js @@ -0,0 +1,50 @@ +odoo.define('website.s_media_list_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.MediaItemLayout = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Change the media item layout. + * + * @see this.selectClass for parameters + */ + layout: function (previewMode, widgetValue, params) { + const $image = this.$target.find('.s_media_list_img_wrapper'); + const $content = this.$target.find('.s_media_list_body'); + + for (const possibleValue of params.possibleValues) { + $image.removeClass(`col-lg-${possibleValue}`); + $content.removeClass(`col-lg-${12 - possibleValue}`); + } + $image.addClass(`col-lg-${widgetValue}`); + $content.addClass(`col-lg-${12 - widgetValue}`); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState(methodName, params) { + switch (methodName) { + case 'layout': { + const $image = this.$target.find('.s_media_list_img_wrapper'); + for (const possibleValue of params.possibleValues) { + if ($image.hasClass(`col-lg-${possibleValue}`)) { + return possibleValue; + } + } + } + } + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_popup/000.js b/addons/website/static/src/snippets/s_popup/000.js new file mode 100644 index 00000000..2b11466e --- /dev/null +++ b/addons/website/static/src/snippets/s_popup/000.js @@ -0,0 +1,119 @@ +odoo.define('website.s_popup', function (require) { +'use strict'; + +const config = require('web.config'); +const publicWidget = require('web.public.widget'); +const utils = require('web.utils'); + +const PopupWidget = publicWidget.Widget.extend({ + selector: '.s_popup', + events: { + 'click .js_close_popup': '_onCloseClick', + 'hide.bs.modal': '_onHideModal', + }, + + /** + * @override + */ + start: function () { + this._popupAlreadyShown = !!utils.get_cookie(this.$el.attr('id')); + if (!this._popupAlreadyShown) { + this._bindPopup(); + } + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + $(document).off('mouseleave.open_popup'); + this.$target.find('.modal').modal('hide'); + clearTimeout(this.timeout); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _bindPopup: function () { + const $main = this.$target.find('.modal'); + + let display = $main.data('display'); + let delay = $main.data('showAfter'); + + if (config.device.isMobile) { + if (display === 'mouseExit') { + display = 'afterDelay'; + delay = 5000; + } + this.$('.modal').removeClass('s_popup_middle').addClass('s_popup_bottom'); + } + + if (display === 'afterDelay') { + this.timeout = setTimeout(() => this._showPopup(), delay); + } else { + $(document).on('mouseleave.open_popup', () => this._showPopup()); + } + }, + /** + * @private + */ + _hidePopup: function () { + this.$target.find('.modal').modal('hide'); + }, + /** + * @private + */ + _showPopup: function () { + if (this._popupAlreadyShown) { + return; + } + this.$target.find('.modal').modal('show'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onCloseClick: function () { + this._hidePopup(); + }, + /** + * @private + */ + _onHideModal: function () { + const nbDays = this.$el.find('.modal').data('consentsDuration'); + utils.set_cookie(this.$el.attr('id'), true, nbDays * 24 * 60 * 60); + this._popupAlreadyShown = true; + }, +}); + +publicWidget.registry.popup = PopupWidget; + +// Prevent bootstrap to prevent scrolling and to add the strange body +// padding-right they add if the popup does not use a backdrop (especially +// important for default cookie bar). +const _baseSetScrollbar = $.fn.modal.Constructor.prototype._setScrollbar; +$.fn.modal.Constructor.prototype._setScrollbar = function () { + if (this._element.classList.contains('s_popup_no_backdrop')) { + return; + } + return _baseSetScrollbar.apply(this, ...arguments); +}; +const _baseGetScrollbarWidth = $.fn.modal.Constructor.prototype._getScrollbarWidth; +$.fn.modal.Constructor.prototype._getScrollbarWidth = function () { + if (this._element.classList.contains('s_popup_no_backdrop')) { + return 0; + } + return _baseGetScrollbarWidth.apply(this, ...arguments); +}; + +return PopupWidget; +}); diff --git a/addons/website/static/src/snippets/s_popup/000.scss b/addons/website/static/src/snippets/s_popup/000.scss new file mode 100644 index 00000000..02738156 --- /dev/null +++ b/addons/website/static/src/snippets/s_popup/000.scss @@ -0,0 +1,99 @@ +// s_popup +.s_popup_main:not([data-vcss]) { + .s_popup_content { + // keep lower than <p> height (cookies bar) + min-height: $o-font-size-base * $o-line-height-base; + box-shadow: $modal-content-box-shadow-sm-up; + .container { + // keep margin when fixed bottom + @include make-container(); + } + } + + &.modal:not(.d-none) { + display: block !important; + } + + $popup-close-size: 1.5rem; + .s_popup_close { + position: absolute; + background: white; + height: $popup-close-size; + width: $popup-close-size; + line-height: $popup-close-size; + margin-top: -1 * $popup-close-size / 2; + margin-right: -1 * $popup-close-size / 2; + border-radius: $popup-close-size / 2; + right: 0px; + top: 0px; + box-shadow: rgba(0,0,0,0.8) 0 0 5px; + cursor: pointer; + text-align: center; + z-index: 1; + font-size: $popup-close-size; + } + + &.s_popup_center { + .s_popup_full { + @include o-position-absolute(0, 0, 0, 0); + &.modal-dialog { + max-width: 100%; + padding: 0 !important; + margin: 0 !important; + + .modal-content { + height: 100%; + width: 100%; + justify-content: center; + } + } + .s_popup_close { + font-size: 60px; + margin: 10px; + background: none; + box-shadow: none; + } + } + } + + &.s_popup_fixed { + &.s_popup_fixed_top { + .s_popup_content { + top: $o-navbar-height; + } + } + &:not(.s_popup_fixed_top) { + .s_popup_content { + bottom: 0; + } + } + .s_popup_content { + z-index: $zindex-modal; + position: fixed; + right: 20px; + } + .modal-sm .s_popup_content { + width: $modal-sm; + } + .modal-md .s_popup_content { + width: $o-modal-md; + } + .modal-lg .s_popup_content { + width: $o-modal-lg; + } + .modal-xl .s_popup_content { + width: $modal-xl; + } + .s_popup_full .s_popup_content { + right: 0; + left: 0; + .s_popup_close { + box-shadow: none; + font-size: 60px; + margin: 10px; + background: none; + } + + } + } +} diff --git a/addons/website/static/src/snippets/s_popup/001.scss b/addons/website/static/src/snippets/s_popup/001.scss new file mode 100644 index 00000000..f8ae94ca --- /dev/null +++ b/addons/website/static/src/snippets/s_popup/001.scss @@ -0,0 +1,72 @@ +.s_popup[data-vcss='001'] { + .modal-content { + min-height: $font-size-lg * 2; + max-height: none; + border: 0; + border-radius: 0; + box-shadow: $modal-content-box-shadow-sm-up; + } + + .modal-dialog { + height: auto; + min-height: 100%; + } + + // Close icon + .s_popup_close { + z-index: $zindex-modal; + @include o-position-absolute(0, 0); + width: $font-size-lg * 2; + height: $font-size-lg * 2; + line-height: $font-size-lg * 2; + @include o-bg-color(color-yiq(o-color('primary')), o-color('primary'), $with-extras: false); + box-shadow: $box-shadow-sm; + cursor: pointer; + font-size: $font-size-lg; + text-align: center; + } + + // Size option - Full + .s_popup_size_full { + padding: 0 !important; + max-width: 100%; + + > .modal-content { + // Use the backdrop color as background-color + background-color: transparent; + box-shadow: none; + border-radius: 0; + } + } + + // Position option - Middle + .s_popup_middle .modal-dialog { + align-items: center; + } + + // Position option - Top/Bottom + .s_popup_top, + .s_popup_bottom { + .modal-dialog { + margin-right: 0; + &:not(.s_popup_size_full) { + padding: $spacer !important; + } + } + } + .s_popup_top .modal-dialog { + align-items: flex-start; + } + .s_popup_bottom .modal-dialog { + align-items: flex-end; + } + + // No backdrop + .s_popup_no_backdrop { + pointer-events: none; + + .modal-content { + pointer-events: auto; + } + } +} diff --git a/addons/website/static/src/snippets/s_popup/options.js b/addons/website/static/src/snippets/s_popup/options.js new file mode 100644 index 00000000..f31c00d3 --- /dev/null +++ b/addons/website/static/src/snippets/s_popup/options.js @@ -0,0 +1,114 @@ +odoo.define('website.s_popup_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.SnippetPopup = options.Class.extend({ + /** + * @override + */ + start: function () { + // Note: the link are excluded here so that internal modal buttons do + // not close the popup as we want to allow edition of those buttons. + this.$target.on('click.SnippetPopup', '.js_close_popup:not(a, .btn)', ev => { + ev.stopPropagation(); + this.onTargetHide(); + this.trigger_up('snippet_option_visibility_update', {show: false}); + }); + this.$target.on('shown.bs.modal.SnippetPopup', () => { + this.trigger_up('snippet_option_visibility_update', {show: true}); + }); + this.$target.on('hidden.bs.modal.SnippetPopup', () => { + this.trigger_up('snippet_option_visibility_update', {show: false}); + }); + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this._super(...arguments); + this.$target.off('.SnippetPopup'); + }, + /** + * @override + */ + onBuilt: function () { + this._assignUniqueID(); + }, + /** + * @override + */ + onClone: function () { + this._assignUniqueID(); + }, + /** + * @override + */ + onTargetShow: async function () { + this.$target.modal('show'); + $(document.body).children('.modal-backdrop:last').addClass('d-none'); + }, + /** + * @override + */ + onTargetHide: async function () { + return new Promise(resolve => { + const timeoutID = setTimeout(() => { + this.$target.off('hidden.bs.modal.popup_on_target_hide'); + resolve(); + }, 500); + this.$target.one('hidden.bs.modal.popup_on_target_hide', () => { + clearTimeout(timeoutID); + resolve(); + }); + this.$target.modal('hide'); + }); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Moves the snippet in footer to be common to all pages + * or inside wrap to be on one page only + * + * @see this.selectClass for parameters + */ + moveBlock: function (previewMode, widgetValue, params) { + const $container = $(widgetValue === 'moveToFooter' ? 'footer' : 'main'); + this.$target.closest('.s_popup').prependTo($container.find('.oe_structure:o_editable').first()); + }, + /** + * @see this.selectClass for parameters + */ + setBackdrop(previewMode, widgetValue, params) { + const color = widgetValue ? 'var(--black-50)' : ''; + this.$target[0].style.setProperty('background-color', color, 'important'); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Creates a unique ID. + * + * @private + */ + _assignUniqueID: function () { + this.$target.closest('.s_popup').attr('id', 'sPopup' + Date.now()); + }, + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'moveBlock': + return this.$target.closest('footer').length ? 'moveToFooter' : 'moveToBody'; + } + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_process_steps/000.scss b/addons/website/static/src/snippets/s_process_steps/000.scss new file mode 100644 index 00000000..3daae1d9 --- /dev/null +++ b/addons/website/static/src/snippets/s_process_steps/000.scss @@ -0,0 +1,52 @@ +.s_process_steps { + .s_process_step_icon { + margin: $grid-gutter-width 0; + span { + display: block; + overflow: hidden; + } + .fa { + display: block; + } + } + .s_process_step_content { + padding: 0 $grid-gutter-width/2; + } + @include media-breakpoint-up(lg) { + overflow-x: hidden; + .s_process_step { + .s_process_step_icon { + position: relative; + z-index: 1; + span:after { + content: ''; + z-index: -1; + border-top: 1px solid gray('500'); + @include o-position-absolute(50%, 0, 0, auto); + } + } + .s_process_step_icon { + span:after { + width: 100%; + } + } + &:first-child .s_process_step_icon, + &:last-child .s_process_step_icon { + span:after { + width: 50%; + } + } + &:first-child .s_process_step_icon { + .fa:after { right: 0; } + .fa.float-right:after { width: 0; } + } + &:last-child .s_process_step_icon { + span:after { left: 0; } + .fa { + &:after { left: 0; } + &.float-left:after { width: 0; } + } + } + } + } +} diff --git a/addons/website/static/src/snippets/s_product_catalog/001.scss b/addons/website/static/src/snippets/s_product_catalog/001.scss new file mode 100644 index 00000000..7e0b2350 --- /dev/null +++ b/addons/website/static/src/snippets/s_product_catalog/001.scss @@ -0,0 +1,29 @@ +.s_product_catalog[data-vcss='001'] { + .s_product_catalog_dish { + // Title + .s_product_catalog_dish_title { + line-height: $headings-line-height; + } + // Description + .s_product_catalog_dish_description { + margin-bottom: $spacer; + } + &:last-child { + .s_product_catalog_dish_description { + margin-bottom: 0; + } + } + // Dot Leaders + .s_product_catalog_dish_dot_leaders { + display: flex; + flex-grow: 1; + align-items: center; + &::after { + content: ''; + margin-left: $spacer/2; + flex: 1 0 auto; + border-bottom: 1px dotted; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_product_catalog/options.js b/addons/website/static/src/snippets/s_product_catalog/options.js new file mode 100644 index 00000000..ef78a364 --- /dev/null +++ b/addons/website/static/src/snippets/s_product_catalog/options.js @@ -0,0 +1,66 @@ + +odoo.define('website.s_product_catalog_options', function (require) { +'use strict'; + +const core = require('web.core'); +const options = require('web_editor.snippets.options'); + +const _t = core._t; + +options.registry.ProductCatalog = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Show/hide descriptions. + * + * @see this.selectClass for parameters + */ + toggleDescription: function (previewMode, widgetValue, params) { + const $dishes = this.$('.s_product_catalog_dish'); + const $name = $dishes.find('.s_product_catalog_dish_name'); + $name.toggleClass('s_product_catalog_dish_dot_leaders', !widgetValue); + if (widgetValue) { + _.each($dishes, el => { + const $description = $(el).find('.s_product_catalog_dish_description'); + if ($description.length) { + $description.removeClass('d-none'); + } else { + const descriptionEl = document.createElement('p'); + descriptionEl.classList.add('s_product_catalog_dish_description', 'border-top', 'text-muted', 'pt-1', 'o_default_snippet_text'); + const iEl = document.createElement('i'); + iEl.textContent = _t("Add a description here"); + descriptionEl.appendChild(iEl); + el.appendChild(descriptionEl); + } + }); + } else { + _.each($dishes, el => { + const $description = $(el).find('.s_product_catalog_dish_description'); + if ($description.hasClass('o_default_snippet_text') || $description.find('.o_default_snippet_text').length) { + $description.remove(); + } else { + $description.addClass('d-none'); + } + }); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + if (methodName === 'toggleDescription') { + const $description = this.$('.s_product_catalog_dish_description'); + return $description.length && !$description.hasClass('d-none'); + } + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_product_list/000.scss b/addons/website/static/src/snippets/s_product_list/000.scss new file mode 100644 index 00000000..480a58ec --- /dev/null +++ b/addons/website/static/src/snippets/s_product_list/000.scss @@ -0,0 +1,45 @@ + +.s_product_list { + padding-top: 20px; + + > div > .row > div { + margin-bottom: 20px; // without this style the columns go directly to the top of the bellow ones. + + height: 200px; + text-align: center; + + a { + display: block; + } + + img { + margin: auto; + max-height: 130px; + @include s-product-list-img-hook; + } + + .s_product_list_item_link { + @include o-position-absolute($left: 10%, $bottom: 0, $right: 10%); + + > .btn { + width: 100%; + padding: 5px !important; + font-size: 16px; + + @media only screen and (max-width : 1280px) { // FIXME + font-size: 12px; + } + + .fa { + font-size: 18px; + padding-right: 5px; + + @media only screen and (max-width : 1024px) { // FIXME + display: block; + font-size: 25px; + } + } + } + } + } +} diff --git a/addons/website/static/src/snippets/s_product_list/000_variables.scss b/addons/website/static/src/snippets/s_product_list/000_variables.scss new file mode 100644 index 00000000..8ce4e819 --- /dev/null +++ b/addons/website/static/src/snippets/s_product_list/000_variables.scss @@ -0,0 +1 @@ +@mixin s-product-list-img-hook {} diff --git a/addons/website/static/src/snippets/s_progress_bar/options.js b/addons/website/static/src/snippets/s_progress_bar/options.js new file mode 100644 index 00000000..d3f544a0 --- /dev/null +++ b/addons/website/static/src/snippets/s_progress_bar/options.js @@ -0,0 +1,80 @@ +odoo.define('website.s_progress_bar_options', function (require) { +'use strict'; + +const core = require('web.core'); +const utils = require('web.utils'); +const options = require('web_editor.snippets.options'); + +const _t = core._t; + +options.registry.progress = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Changes the position of the progressbar text. + * + * @see this.selectClass for parameters + */ + display: function (previewMode, widgetValue, params) { + // retro-compatibility + if (this.$target.hasClass('progress')) { + this.$target.removeClass('progress'); + this.$target.find('.progress-bar').wrap($('<div/>', { + class: 'progress', + })); + this.$target.find('.progress-bar span').addClass('s_progress_bar_text'); + } + + let $text = this.$target.find('.s_progress_bar_text'); + if (!$text.length) { + $text = $('<span/>').addClass('s_progress_bar_text').html(_t('80% Development')); + } + + if (widgetValue === 'inline') { + $text.appendTo(this.$target.find('.progress-bar')); + } else { + $text.insertBefore(this.$target.find('.progress')); + } + }, + /** + * Sets the progress bar value. + * + * @see this.selectClass for parameters + */ + progressBarValue: function (previewMode, widgetValue, params) { + let value = parseInt(widgetValue); + value = utils.confine(value, 0, 100); + const $progressBar = this.$target.find('.progress-bar'); + const $progressBarText = this.$target.find('.s_progress_bar_text'); + // Target precisely the XX% not only XX to not replace wrong element + // eg 'Since 1978 we have completed 45%' <- don't replace 1978 + $progressBarText.text($progressBarText.text().replace(/[0-9]+%/, value + '%')); + $progressBar.attr("aria-valuenow", value); + $progressBar.css("width", value + "%"); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'display': { + const isInline = this.$target.find('.s_progress_bar_text') + .parent('.progress-bar').length; + return isInline ? 'inline' : 'below'; + } + case 'progressBarValue': { + return this.$target.find('.progress-bar').attr('aria-valuenow') + '%'; + } + } + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_quotes_carousel/000.scss b/addons/website/static/src/snippets/s_quotes_carousel/000.scss new file mode 100644 index 00000000..8da0a16c --- /dev/null +++ b/addons/website/static/src/snippets/s_quotes_carousel/000.scss @@ -0,0 +1,22 @@ +.s_quotes_carousel:not([data-vcss]) { + blockquote { + padding: $grid-gutter-width; + margin-bottom: 0; + .s_quotes_carousel_icon { + position: absolute; + top: 0; + left: -3rem; + } + img { + max-width: 40px; + margin-right: 5px; + border-radius: 50%; + } + footer { + background-color: transparent; + &:before { + content:""; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_quotes_carousel/001.scss b/addons/website/static/src/snippets/s_quotes_carousel/001.scss new file mode 100644 index 00000000..c26dff12 --- /dev/null +++ b/addons/website/static/src/snippets/s_quotes_carousel/001.scss @@ -0,0 +1,8 @@ +.s_quotes_carousel_wrapper[data-vcss='001'] { + .s_blockquote { + margin-bottom: 0; + @include media-breakpoint-down(sm) { + width: 100% !important; // TODO add the right class in the xml when it's possible + } + } +} diff --git a/addons/website/static/src/snippets/s_rating/000.scss b/addons/website/static/src/snippets/s_rating/000.scss new file mode 100644 index 00000000..968ee7d3 --- /dev/null +++ b/addons/website/static/src/snippets/s_rating/000.scss @@ -0,0 +1,55 @@ + +.s_rating:not([data-vcss]) { + $star: "\f005"; + $star-o: "\f006"; + $circle: "\f111"; + $circle-o: "\f10c"; + $heart: "\f004"; + $heart-o: "\f08a"; + @mixin s_rating_generate_icons($off, $on) { + .fa:before { + content: $off; + } + @for $counter from 5 to 0 { + &.s_rating_#{$counter} { + .fa:nth-of-type(-n+#{$counter}):before { + content: $on; + } + } + } + } + > .s_rating_stars { @include s_rating_generate_icons($star-o, $star); } + > .s_rating_squares { @include s_rating_generate_icons($circle-o, $circle); } + > .s_rating_hearts { @include s_rating_generate_icons($heart-o, $heart); } + > .s_rating_bar { + .fa { + display: none; + } + .s_rating_bar { + display: flex; + height: $progress-height; + background-color: $gray-300; + &:before { + content: ""; + display: flex; + flex-direction: column; + justify-content: center; + @include transition($progress-bar-transition); + @include gradient-striped(); + background-size: $progress-height $progress-height; + background-color: theme-color('primary'); + animation: progress-bar-stripes $progress-bar-animation-timing; + } + } + @for $counter from 5 to 0 { + &.s_rating_#{$counter} { + .s_rating_bar:before { + width: percentage($counter/5); + } + } + } + } + > .s_rating_1x { .fa { font-size: 1em; }; } + > .s_rating_2x { .fa { font-size: 2em; }; } + > .s_rating_3x { .fa { font-size: 3em; }; } +} diff --git a/addons/website/static/src/snippets/s_rating/001.scss b/addons/website/static/src/snippets/s_rating/001.scss new file mode 100644 index 00000000..a1fc87c0 --- /dev/null +++ b/addons/website/static/src/snippets/s_rating/001.scss @@ -0,0 +1,15 @@ + +.s_rating[data-vcss="001"] { + &.s_rating_inline { + display: flex; + align-items: center; + + .s_rating_title { + margin: 0; + margin-right: 0.5em; + } + .s_rating_icons { + margin-left: auto; + } + } +} diff --git a/addons/website/static/src/snippets/s_rating/options.js b/addons/website/static/src/snippets/s_rating/options.js new file mode 100644 index 00000000..b24d7daf --- /dev/null +++ b/addons/website/static/src/snippets/s_rating/options.js @@ -0,0 +1,151 @@ +odoo.define('website.s_rating_options', function (require) { +'use strict'; + +const weWidgets = require('wysiwyg.widgets'); +const options = require('web_editor.snippets.options'); + +options.registry.Rating = options.Class.extend({ + /** + * @override + */ + start: function () { + this.iconType = this.$target[0].dataset.icon; + this.faClassActiveCustomIcons = this.$target[0].dataset.activeCustomIcon || ''; + this.faClassInactiveCustomIcons = this.$target[0].dataset.inactiveCustomIcon || ''; + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Displays the selected icon type. + * + * @see this.selectClass for parameters + */ + setIcons: function (previewMode, widgetValue, params) { + this.iconType = widgetValue; + this._renderIcons(); + this.$target[0].dataset.icon = widgetValue; + delete this.$target[0].dataset.activeCustomIcon; + delete this.$target[0].dataset.inactiveCustomIcon; + }, + /** + * Allows to select a font awesome icon with media dialog. + * + * @see this.selectClass for parameters + */ + customIcon: async function (previewMode, widgetValue, params) { + return new Promise(resolve => { + const dialog = new weWidgets.MediaDialog( + this, + {noImages: true, noDocuments: true, noVideos: true, mediaWidth: 1920}, + $('<i/>') + ); + this._saving = false; + dialog.on('save', this, function (attachments) { + this._saving = true; + const customClass = 'fa ' + attachments.className; + const $activeIcons = this.$target.find('.s_rating_active_icons > i'); + const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i'); + const $icons = params.customActiveIcon === 'true' ? $activeIcons : $inactiveIcons; + $icons.removeClass().addClass(customClass); + this.faClassActiveCustomIcons = $activeIcons.length > 0 ? $activeIcons.attr('class') : customClass; + this.faClassInactiveCustomIcons = $inactiveIcons.length > 0 ? $inactiveIcons.attr('class') : customClass; + this.$target[0].dataset.activeCustomIcon = this.faClassActiveCustomIcons; + this.$target[0].dataset.inactiveCustomIcon = this.faClassInactiveCustomIcons; + this.$target[0].dataset.icon = 'custom'; + this.iconType = 'custom'; + resolve(); + }); + dialog.on('closed', this, function () { + if (!this._saving) { + resolve(); + } + }); + dialog.open(); + }); + }, + /** + * Sets the number of active icons. + * + * @see this.selectClass for parameters + */ + activeIconsNumber: function (previewMode, widgetValue, params) { + this.nbActiveIcons = parseInt(widgetValue); + this._createIcons(); + }, + /** + * Sets the total number of icons. + * + * @see this.selectClass for parameters + */ + totalIconsNumber: function (previewMode, widgetValue, params) { + this.nbTotalIcons = Math.max(parseInt(widgetValue), 1); + this._createIcons(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'setIcons': { + return this.$target[0].dataset.icon; + } + case 'activeIconsNumber': { + this.nbActiveIcons = this.$target.find('.s_rating_active_icons > i').length; + return this.nbActiveIcons; + } + case 'totalIconsNumber': { + this.nbTotalIcons = this.$target.find('.s_rating_icons i').length; + return this.nbTotalIcons; + } + } + return this._super(...arguments); + }, + /** + * Creates the icons. + * + * @private + */ + _createIcons: function () { + const $activeIcons = this.$target.find('.s_rating_active_icons'); + const $inactiveIcons = this.$target.find('.s_rating_inactive_icons'); + this.$target.find('.s_rating_icons i').remove(); + for (let i = 0; i < this.nbTotalIcons; i++) { + if (i < this.nbActiveIcons) { + $activeIcons.append('<i/> '); + } else { + $inactiveIcons.append('<i/> '); + } + } + this._renderIcons(); + }, + /** + * Renders icons with selected fonts. + * + * @private + */ + _renderIcons: function () { + const icons = { + 'fa-star': 'fa-star-o', + 'fa-thumbs-up': 'fa-thumbs-o-up', + 'fa-circle': 'fa-circle-o', + 'fa-square': 'fa-square-o', + 'fa-heart': 'fa-heart-o' + }; + const faClassActiveIcons = (this.iconType === "custom") ? this.faClassActiveCustomIcons : 'fa ' + this.iconType; + const faClassInactiveIcons = (this.iconType === "custom") ? this.faClassInactiveCustomIcons : 'fa ' + icons[this.iconType]; + const $activeIcons = this.$target.find('.s_rating_active_icons > i'); + const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i'); + $activeIcons.removeClass().addClass(faClassActiveIcons); + $inactiveIcons.removeClass().addClass(faClassInactiveIcons); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_references/000.scss b/addons/website/static/src/snippets/s_references/000.scss new file mode 100644 index 00000000..c4a00320 --- /dev/null +++ b/addons/website/static/src/snippets/s_references/000.scss @@ -0,0 +1,4 @@ + +.s_references .img-thumbnail { + border: none; +} diff --git a/addons/website/static/src/snippets/s_share/000.js b/addons/website/static/src/snippets/s_share/000.js new file mode 100644 index 00000000..b6d630b1 --- /dev/null +++ b/addons/website/static/src/snippets/s_share/000.js @@ -0,0 +1,49 @@ +odoo.define('website.s_share', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); + +const ShareWidget = publicWidget.Widget.extend({ + selector: '.s_share, .oe_share', // oe_share for compatibility + + /** + * @override + */ + start: function () { + var urlRegex = /(\?(?:|.*&)(?:u|url|body)=)(.*?)(&|#|$)/; + var titleRegex = /(\?(?:|.*&)(?:title|text|subject)=)(.*?)(&|#|$)/; + var url = encodeURIComponent(window.location.href); + var title = encodeURIComponent($('title').text()); + this.$('a').each(function () { + var $a = $(this); + $a.attr('href', function (i, href) { + return href.replace(urlRegex, function (match, a, b, c) { + return a + url + c; + }).replace(titleRegex, function (match, a, b, c) { + if ($a.hasClass('s_share_whatsapp')) { + // WhatsApp does not support the "url" GET parameter. + // Instead we need to include the url within the passed "text" parameter, merging everything together. + // e.g of output: + // https://wa.me/?text=%20OpenWood%20Collection%20Online%20Reveal%20%7C%20My%20Website%20http%3A%2F%2Flocalhost%3A8888%2Fevent%2Fopenwood-collection-online-reveal-2021-06-21-2021-06-23-8%2Fregister + // see https://faq.whatsapp.com/general/chats/how-to-use-click-to-chat/ for more details + return a + title + url + c; + } + return a + title + c; + }); + }); + if ($a.attr('target') && $a.attr('target').match(/_blank/i) && !$a.closest('.o_editable').length) { + $a.on('click', function () { + window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600'); + return false; + }); + } + }); + + return this._super.apply(this, arguments); + }, +}); + +publicWidget.registry.share = ShareWidget; + +return ShareWidget; +}); diff --git a/addons/website/static/src/snippets/s_share/000.scss b/addons/website/static/src/snippets/s_share/000.scss new file mode 100644 index 00000000..85ab9edd --- /dev/null +++ b/addons/website/static/src/snippets/s_share/000.scss @@ -0,0 +1,65 @@ + +.s_share { + > * { + display: inline-block; + vertical-align: middle; + } + .s_share_title { + margin: 0 .4rem 0 0; + } + a { + i.fa { + display: flex; + justify-content: center; + align-items: center; + } + margin: .2rem; + } + &:not(.no_icon_color) { + .s_share_facebook { + &, &:hover, &:focus { + @extend .text-facebook; + } + } + .s_share_twitter { + &, &:hover, &:focus { + @extend .text-twitter; + } + } + .s_share_linkedin { + &, &:hover, &:focus { + @extend .text-linkedin; + } + } + .s_share_google { + &, &:hover, &:focus { + @extend .text-google-plus; + } + } + .s_share_whatsapp { + &, &:hover, &:focus { + @extend .text-whatsapp; + } + } + .s_share_pinterest { + &, &:hover, &:focus { + @extend .text-pinterest; + } + } + .s_share_github { + &, &:hover, &:focus { + @extend .text-github; + } + } + .s_share_instagram { + &, &:hover, &:focus { + @extend .text-instagram; + } + } + .s_share_youtube { + &, &:hover, &:focus { + @extend .text-youtube; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_showcase/000.scss b/addons/website/static/src/snippets/s_showcase/000.scss new file mode 100644 index 00000000..8970e7e2 --- /dev/null +++ b/addons/website/static/src/snippets/s_showcase/000.scss @@ -0,0 +1,77 @@ + +#wrapwrap .s_showcase:not([data-vcss]) { + @include media-breakpoint-up(lg) { + .container, .container-fluid { + position: relative; + + &:before { + content: " "; + display: block; + @include o-position-absolute($left: 50%); + height: 100%; + border-right: 1px solid gray('200'); + } + } + } + + .fa { + opacity: 0.5; + } + + .text-right{ + .fa { + float: right; + margin-left: .5em; + } + p { + float: right; + display: block; + } + } + .text-left{ + .fa { + float: left; + margin-right: .5em; + } + p { + float: left; + } + } + .row { + margin-top: 1em; + } + .feature p { + max-width: 300px; + margin-top: 0.6em; + clear: both; + } +} + +@include media-breakpoint-down(md) { + #wrapwrap .s_showcase:not([data-vcss]) { + .text-right, .text-left { + text-align: center; + + .fa { + font-size: 2em; + opacity: 0.5; + float: none; + display: block; + position: relative; + margin-left: auto; + margin-right: auto; + } + } + .feature { + margin-bottom: 3em; + + p { + float: none; + display: block; + position: relative; + margin-left: auto; + margin-right: auto; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_showcase/001.scss b/addons/website/static/src/snippets/s_showcase/001.scss new file mode 100644 index 00000000..f7a8cd3f --- /dev/null +++ b/addons/website/static/src/snippets/s_showcase/001.scss @@ -0,0 +1,46 @@ +.s_showcase[data-vcss='001'] { + @include media-breakpoint-up(lg) { + // Left-right separator + .container, .container-fluid { + position: relative; + + &:before { + content: " "; + display: block; + @include o-position-absolute($left: 50%); + height: 100%; + border-right: 1px solid gray('200'); + } + } + // Features + .row > div { + // Items on left + &:nth-child(odd) { + text-align: right; + + .s_showcase_icon, p { + float: right; + } + + .s_showcase_icon { + margin-right: 0; + margin-left: 15px; + } + } + // Items on right + &:nth-child(even) { + text-align: left; + + .s_showcase_icon, p { + float: left; + } + } + } + } + + .s_showcase_icon { + // Make the default margin the one for the left aligned icon, as it's what we want on mobile + margin-right: 15px; + font-size: 36px; + } +} diff --git a/addons/website/static/src/snippets/s_showcase/options.js b/addons/website/static/src/snippets/s_showcase/options.js new file mode 100644 index 00000000..b870393d --- /dev/null +++ b/addons/website/static/src/snippets/s_showcase/options.js @@ -0,0 +1,19 @@ +odoo.define('website.s_showcase_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.Showcase = options.Class.extend({ + /** + * @override + */ + onMove: function () { + const $showcaseCol = this.$target.parent().closest('.row > div'); + const isLeftCol = $showcaseCol.index() <= 0; + const $title = this.$target.children('.s_showcase_title'); + $title.toggleClass('flex-lg-row-reverse', isLeftCol); + $showcaseCol.find('.s_showcase_icon.ml-3').removeClass('ml-3').addClass('ml-lg-3'); // For compatibility with old version + $title.find('.s_showcase_icon').toggleClass('mr-lg-0 ml-lg-3', isLeftCol); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_table_of_content/000.js b/addons/website/static/src/snippets/s_table_of_content/000.js new file mode 100644 index 00000000..fcae4ed7 --- /dev/null +++ b/addons/website/static/src/snippets/s_table_of_content/000.js @@ -0,0 +1,77 @@ +odoo.define('website.s_table_of_content', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); +const {extraMenuUpdateCallbacks} = require('website.content.menu'); + +const TableOfContent = publicWidget.Widget.extend({ + selector: 'section .s_table_of_content_navbar_sticky', + disabledInEditableMode: false, + + /** + * @override + */ + async start() { + await this._super(...arguments); + this._updateTableOfContentNavbarPosition(); + extraMenuUpdateCallbacks.push(this._updateTableOfContentNavbarPosition.bind(this)); + }, + /** + * @override + */ + destroy() { + this.$target.css('top', ''); + this.$target.find('.s_table_of_content_navbar').css('top', ''); + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _updateTableOfContentNavbarPosition() { + let position = 0; + const $fixedElements = $('.o_top_fixed_element'); + _.each($fixedElements, el => position += $(el).outerHeight()); + const isHorizontalNavbar = this.$target.hasClass('s_table_of_content_horizontal_navbar'); + this.$target.css('top', isHorizontalNavbar ? position : ''); + this.$target.find('.s_table_of_content_navbar').css('top', isHorizontalNavbar ? '' : position + 20); + const $mainNavBar = $('#oe_main_menu_navbar'); + position += $mainNavBar.length ? $mainNavBar.outerHeight() : 0; + position += isHorizontalNavbar ? this.$target.outerHeight() : 0; + $().getScrollingElement().scrollspy({target: '.s_table_of_content_navbar', method: 'offset', offset: position + 100, alwaysKeepFirstActive: true}); + }, +}); + +publicWidget.registry.anchorSlide.include({ + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Overridden to add the height of the horizontal sticky navbar at the scroll value + * when the link is from the table of content navbar + * + * @override + * @private + */ + _computeExtraOffset() { + let extraOffset = this._super(...arguments); + if (this.$el.hasClass('table_of_content_link')) { + const tableOfContentNavbarEl = this.$el.closest('.s_table_of_content_navbar_sticky.s_table_of_content_horizontal_navbar'); + if (tableOfContentNavbarEl.length > 0) { + extraOffset += $(tableOfContentNavbarEl).outerHeight(); + } + } + return extraOffset; + }, +}); + +publicWidget.registry.snippetTableOfContent = TableOfContent; + +return TableOfContent; +}); diff --git a/addons/website/static/src/snippets/s_table_of_content/000.scss b/addons/website/static/src/snippets/s_table_of_content/000.scss new file mode 100644 index 00000000..670e1365 --- /dev/null +++ b/addons/website/static/src/snippets/s_table_of_content/000.scss @@ -0,0 +1,65 @@ +.s_table_of_content:not([data-vcss]) { + .s_table_of_content_navbar_wrap { + &.s_table_of_content_navbar_sticky { + &.s_table_of_content_horizontal_navbar, &.s_table_of_content_vertical_navbar .s_table_of_content_navbar { + @include o-position-sticky($top: 0px); + } + } + &:not(.s_table_of_content_navbar_sticky) { + &, .s_table_of_content_navbar { + top: 0px !important; + } + } + &.s_table_of_content_vertical_navbar .s_table_of_content_navbar { + > a.list-group-item-action { + background: none; + color: inherit; + opacity: 0.7; + font-weight: $font-weight-normal + 100; + padding-left: 3px; + transition: padding 0.1s; + + &:before { + @include o-position-absolute(10px, auto, 10px, 0); + width: 2px; + content: ""; + } + &:hover { + opacity: 1; + } + &:focus { + background: none; + } + &.active { + background: none; + padding-left: 8px; + opacity: 1; + + &:before { + background-color: theme-color('primary'); + } + } + } + } + &.s_table_of_content_horizontal_navbar { + z-index: 1; + padding-top: $navbar-padding-y; + padding-bottom: $navbar-padding-y; + margin-bottom: $spacer * 2; + + .s_table_of_content_navbar { + display: inline; + + > a { + &.list-group-item-action { + width: auto; + } + &.list-group-item { + display: inline-block; + margin-bottom: 2px; + } + } + } + } + } +} diff --git a/addons/website/static/src/snippets/s_table_of_content/options.js b/addons/website/static/src/snippets/s_table_of_content/options.js new file mode 100644 index 00000000..3feb0c74 --- /dev/null +++ b/addons/website/static/src/snippets/s_table_of_content/options.js @@ -0,0 +1,122 @@ +odoo.define('website.s_table_of_content_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.TableOfContent = options.Class.extend({ + /** + * @override + */ + start: function () { + this.targetedElements = 'h1, h2'; + const $headings = this.$target.find(this.targetedElements); + if ($headings.length > 0) { + this._generateNav(); + } + // Generate the navbar if the content changes + const targetNode = this.$target.find('.s_table_of_content_main')[0]; + const config = {attributes: false, childList: true, subtree: true, characterData: true}; + this.observer = new MutationObserver(() => this._generateNav()); + this.observer.observe(targetNode, config); + return this._super(...arguments); + }, + /** + * @override + */ + onClone: function () { + this._generateNav(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _generateNav: function (ev) { + const $nav = this.$target.find('.s_table_of_content_navbar'); + const $headings = this.$target.find(this.targetedElements); + $nav.empty(); + _.each($headings, el => { + const $el = $(el); + const id = 'table_of_content_heading_' + _.now() + '_' + _.uniqueId(); + $('<a>').attr('href', "#" + id) + .addClass('table_of_content_link list-group-item list-group-item-action py-2 border-0 rounded-0') + .text($el.text()) + .appendTo($nav); + $el.attr('id', id); + $el[0].dataset.anchor = 'true'; + }); + $nav.find('a:first').addClass('active'); + }, +}); + +options.registry.TableOfContentNavbar = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Change the navbar position. + * + * @see this.selectClass for parameters + */ + navbarPosition: function (previewMode, widgetValue, params) { + const $navbar = this.$target; + const $mainContent = this.$target.parent().find('.s_table_of_content_main'); + if (widgetValue === 'top' || widgetValue === 'left') { + $navbar.prev().before($navbar); + } + if (widgetValue === 'left' || widgetValue === 'right') { + $navbar.removeClass('s_table_of_content_horizontal_navbar col-lg-12').addClass('s_table_of_content_vertical_navbar col-lg-3'); + $mainContent.removeClass('col-lg-12').addClass('col-lg-9'); + $navbar.find('.s_table_of_content_navbar').removeClass('list-group-horizontal-md'); + } + if (widgetValue === 'right') { + $navbar.next().after($navbar); + } + if (widgetValue === 'top') { + $navbar.removeClass('s_table_of_content_vertical_navbar col-lg-3').addClass('s_table_of_content_horizontal_navbar col-lg-12'); + $navbar.find('.s_table_of_content_navbar').addClass('list-group-horizontal-md'); + $mainContent.removeClass('col-lg-9').addClass('col-lg-12'); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'navbarPosition': { + const $navbar = this.$target; + if ($navbar.hasClass('s_table_of_content_horizontal_navbar')) { + return 'top'; + } else { + const $mainContent = $navbar.parent().find('.s_table_of_content_main'); + return $navbar.prev().is($mainContent) === true ? 'right' : 'left'; + } + } + } + return this._super(...arguments); + }, +}); + +options.registry.TableOfContentMainColumns = options.Class.extend({ + forceNoDeleteButton: true, + + /** + * @override + */ + start: function () { + const leftPanelEl = this.$overlay.data('$optionsSection')[0]; + leftPanelEl.querySelector('.oe_snippet_clone').classList.add('d-none'); // TODO improve the way to do that + return this._super.apply(this, arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_tabs/001.scss b/addons/website/static/src/snippets/s_tabs/001.scss new file mode 100644 index 00000000..6e132e91 --- /dev/null +++ b/addons/website/static/src/snippets/s_tabs/001.scss @@ -0,0 +1,25 @@ +// Tabs +.s_tabs[data-vcss="001"] { + .s_tabs_content { + &.s_tabs_slide_up, &.s_tabs_slide_down, &.s_tabs_slide_left, &.s_tabs_slide_right { + > .tab-pane.fade { + transition: all 0.2s; + } + > .tab-pane.fade.show { + transform: translateX(0rem) translateY(0rem); + } + } + &.s_tabs_slide_up > .tab-pane.fade { + transform: translateY(-1rem); + } + &.s_tabs_slide_down > .tab-pane.fade { + transform: translateY(1rem); + } + &.s_tabs_slide_left > .tab-pane.fade { + transform: translateX(-1rem); + } + &.s_tabs_slide_right > .tab-pane.fade { + transform: translateX(1rem); + } + } +} diff --git a/addons/website/static/src/snippets/s_tabs/options.js b/addons/website/static/src/snippets/s_tabs/options.js new file mode 100644 index 00000000..b10a7619 --- /dev/null +++ b/addons/website/static/src/snippets/s_tabs/options.js @@ -0,0 +1,167 @@ +odoo.define('website.s_tabs_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.NavTabs = options.Class.extend({ + isTopOption: true, + + /** + * @override + */ + start: function () { + this._findLinksAndPanes(); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + onBuilt: function () { + this._generateUniqueIDs(); + }, + /** + * @override + */ + onClone: function () { + this._generateUniqueIDs(); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Creates a new tab and tab-pane. + * + * @see this.selectClass for parameters + */ + addTab: function (previewMode, widgetValue, params) { + var $activeItem = this.$navLinks.filter('.active').parent(); + var $activePane = this.$tabPanes.filter('.active'); + + var $navItem = $activeItem.clone(); + var $navLink = $navItem.find('.nav-link').removeClass('active show'); + var $tabPane = $activePane.clone().removeClass('active show'); + $navItem.insertAfter($activeItem); + $tabPane.insertAfter($activePane); + this._findLinksAndPanes(); + this._generateUniqueIDs(); + + $navLink.tab('show'); + }, + /** + * Removes the current active tab and its content. + * + * @see this.selectClass for parameters + */ + removeTab: function (previewMode, widgetValue, params) { + var self = this; + + var $activeLink = this.$navLinks.filter('.active'); + var $activePane = this.$tabPanes.filter('.active'); + + var $next = this.$navLinks.eq((this.$navLinks.index($activeLink) + 1) % this.$navLinks.length); + + return new Promise(resolve => { + $next.one('shown.bs.tab', function () { + $activeLink.parent().remove(); + $activePane.remove(); + self._findLinksAndPanes(); + resolve(); + }); + $next.tab('show'); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetVisibility: async function (widgetName, params) { + if (widgetName === 'remove_tab_opt') { + return (this.$tabPanes.length > 2); + } + return this._super(...arguments); + }, + /** + * @private + */ + _findLinksAndPanes: function () { + this.$navLinks = this.$target.find('.nav:first .nav-link'); + this.$tabPanes = this.$target.find('.tab-content:first .tab-pane'); + }, + /** + * @private + */ + _generateUniqueIDs: function () { + for (var i = 0; i < this.$navLinks.length; i++) { + var id = _.now() + '_' + _.uniqueId(); + var idLink = 'nav_tabs_link_' + id; + var idContent = 'nav_tabs_content_' + id; + this.$navLinks.eq(i).attr({ + 'id': idLink, + 'href': '#' + idContent, + 'aria-controls': idContent, + }); + this.$tabPanes.eq(i).attr({ + 'id': idContent, + 'aria-labelledby': idLink, + }); + } + }, +}); +options.registry.NavTabsStyle = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Set the style of the tabs. + * + * @see this.selectClass for parameters + */ + setStyle: function (previewMode, widgetValue, params) { + const $nav = this.$target.find('.s_tabs_nav:first .nav'); + const isPills = widgetValue === 'pills'; + $nav.toggleClass('nav-tabs card-header-tabs', !isPills); + $nav.toggleClass('nav-pills', isPills); + this.$target.find('.s_tabs_nav:first').toggleClass('card-header', !isPills).toggleClass('mb-3', isPills); + this.$target.toggleClass('card', !isPills); + this.$target.find('.s_tabs_content:first').toggleClass('card-body', !isPills); + }, + /** + * Horizontal/vertical nav. + * + * @see this.selectClass for parameters + */ + setDirection: function (previewMode, widgetValue, params) { + const isVertical = widgetValue === 'vertical'; + this.$target.toggleClass('row s_col_no_resize s_col_no_bgcolor', isVertical); + this.$target.find('.s_tabs_nav:first .nav').toggleClass('flex-column', isVertical); + this.$target.find('.s_tabs_nav:first > .nav-link').toggleClass('py-2', isVertical); + this.$target.find('.s_tabs_nav:first').toggleClass('col-md-3', isVertical); + this.$target.find('.s_tabs_content:first').toggleClass('col-md-9', isVertical); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'setStyle': + return this.$target.find('.s_tabs_nav:first .nav').hasClass('nav-pills') ? 'pills' : 'tabs'; + case 'setDirection': + return this.$target.find('.s_tabs_nav:first .nav').hasClass('flex-column') ? 'vertical' : 'horizontal'; + } + return this._super(...arguments); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_text_highlight/000.scss b/addons/website/static/src/snippets/s_text_highlight/000.scss new file mode 100644 index 00000000..913f04bb --- /dev/null +++ b/addons/website/static/src/snippets/s_text_highlight/000.scss @@ -0,0 +1,7 @@ +.s_text_highlight { + padding: 1.5rem; + border-radius: $border-radius; + :last-child { + margin-bottom: 0; + } +} diff --git a/addons/website/static/src/snippets/s_three_columns/000.scss b/addons/website/static/src/snippets/s_three_columns/000.scss new file mode 100644 index 00000000..24e5224e --- /dev/null +++ b/addons/website/static/src/snippets/s_three_columns/000.scss @@ -0,0 +1,5 @@ +.s_three_columns:not([data-vcss]) { + .align-items-stretch > .card { + height: 100%; + } +} diff --git a/addons/website/static/src/snippets/s_timeline/000.scss b/addons/website/static/src/snippets/s_timeline/000.scss new file mode 100644 index 00000000..4a0ce4df --- /dev/null +++ b/addons/website/static/src/snippets/s_timeline/000.scss @@ -0,0 +1,70 @@ +.s_timeline { + .s_timeline_line { + position: relative; + &:before { + content: ''; + display: block !important; // override portal '#wrap .container' value + position: absolute; + width: 1px; + top: 0px; + bottom: 0px; + left: 50%; + background-color: gray('800'); + } + } + .s_timeline_row { + align-items: center; + .s_timeline_content { + align-items: center; + justify-content: flex-end; + width: 100%; + ~ .s_timeline_content { + justify-content: flex-start; + } + } + &.flex-row-reverse { + .s_timeline_content { + flex-direction: row-reverse; + } + } + @include media-breakpoint-up(md) { + &.flex-row-reverse { + .s_timeline_content { + flex-direction: row-reverse; + &:not(:last-child) { + margin-left: 10%; + } + } + } + &:not(.flex-row-reverse) { + .s_timeline_content:last-child { + margin-left: 10%; + } + } + } + } + .s_timeline_date { + @include media-breakpoint-up(md) { + position: absolute; + left: 0%; + right: 0%; + } + @include media-breakpoint-down(sm) { + position: relative; + margin: 20px 0px; + } + span:not(.fa) { + display: inline-block; + padding: 5px; + } + .fa { + margin: 0 $grid-gutter-width/2; + } + text-align: center; + } + .s_timeline_icon { + flex: 0 0 auto; + margin: $grid-gutter-width/2; + z-index: 1; + } +} diff --git a/addons/website/static/src/snippets/s_timeline/options.js b/addons/website/static/src/snippets/s_timeline/options.js new file mode 100644 index 00000000..c1efa23c --- /dev/null +++ b/addons/website/static/src/snippets/s_timeline/options.js @@ -0,0 +1,32 @@ +odoo.define('website.s_timeline_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.Timeline = options.Class.extend({ + /** + * @override + */ + start: function () { + var $buttons = this.$el.find('we-button'); + var $overlayArea = this.$overlay.find('.o_overlay_options_wrap'); + $overlayArea.append($('<div/>').append($buttons)); + + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Moves the card to the right/left. + * + * @see this.selectClass for parameters + */ + timelineCard: function (previewMode, widgetValue, params) { + const $timelineRow = this.$target.closest('.s_timeline_row'); + $timelineRow.toggleClass('flex-row-reverse flex-row'); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_title/000.scss b/addons/website/static/src/snippets/s_title/000.scss new file mode 100644 index 00000000..a4e76dca --- /dev/null +++ b/addons/website/static/src/snippets/s_title/000.scss @@ -0,0 +1,36 @@ + +.s_title:not([data-vcss]) { + .s_title_boxed { + > * { + display: inline-block; + padding: $grid-gutter-width; + border: 1px solid; + } + } + .s_title_lines { + overflow: hidden; + &:before, + &:after { + content: ""; + display: inline-block; + vertical-align: middle; + width: 100%; + border-top: 1px solid; + border-top-color: inherit; + } + &:before { margin: 0 $grid-gutter-width/2 0 -100%; } + &:after { margin: 0 -100% 0 $grid-gutter-width/2; } + } + .s_title_underlined { + @extend %o-page-header; + } + .s_title_small_caps { + font-variant: small-caps; + } + .s_title_transparent { + opacity: .5; + } + .s_title_thin { + font-weight: 300; + } +} diff --git a/addons/website/static/src/xml/theme_preview.xml b/addons/website/static/src/xml/theme_preview.xml new file mode 100644 index 00000000..67ce152e --- /dev/null +++ b/addons/website/static/src/xml/theme_preview.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="website.ThemePreview.Buttons"> + <div> + <div class="o_form_buttons_view" role="toolbar" aria-label="Main actions"> + <button type="object" name="button_choose_theme" class="btn btn-primary o_use_theme"> + Start Now + </button> + <button class="btn btn-link o_switch_theme"> + Choose another theme + </button> + </div> + </div> + </t> + + <t t-name="website.ThemePreview.SwitchModeButton"> + <div class="btn-group btn-group-toggle ml-1" data-toggle="buttons"> + <label class="btn btn-secondary active"> + <input type="radio" name="viewer" data-mode='desktop' autocomplete="off" checked='checked'/> Desktop + </label> + <label class="btn btn-secondary"> + <input type="radio" name="viewer" data-mode='mobile' autocomplete="off"/> Mobile + </label> + </div> + </t> + <t t-name="website.ThemePreview.Loader"> + <div class="o_theme_install_loader_container position-fixed fixed-top fixed-left + h-100 w-100 d-flex flex-column align-items-center text-white font-weight-bold"> + <t t-if="showTips">Building your website...</t> + <div class="o_theme_install_loader"></div> + <p t-if="showTips" class="o_theme_install_loader_tip text-center w-25"> + TIP: Follow the + <span class="o_tooltip o_tooltip_visible top o_animated position-relative"></span> + to build the perfect page in 5 steps. + </p> + </div> + </t> +</templates> diff --git a/addons/website/static/src/xml/track_page.xml b/addons/website/static/src/xml/track_page.xml new file mode 100644 index 00000000..2d4093dc --- /dev/null +++ b/addons/website/static/src/xml/track_page.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="website.track_page"> + <div role="separator" class="dropdown-divider"/> + <a href="#" name="switch-track-page" class="dropdown-item" role="menuitem"> + <label class="o_switch" for="switch-track-page"> + <input id="switch-track-page" type="checkbox"/> + <span/> + Track Visitor + </label> + </a> + </t> + +</templates> diff --git a/addons/website/static/src/xml/translator.xml b/addons/website/static/src/xml/translator.xml new file mode 100644 index 00000000..d3067f5e --- /dev/null +++ b/addons/website/static/src/xml/translator.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> +<div t-name="website.TranslatorInfoDialog"> + <p>You are about to enter the translation mode.</p> + <p>Here are the visuals used to help you translate efficiently:</p> + <ul class="oe_translate_examples"> + <li data-oe-translation-state="to_translate">Content to translate</li> + <li data-oe-translation-state="translated">Translated content</li> + </ul> + <p> + In this mode, you can only translate texts. To change the structure of the page, you must edit the master page. + Each modification on the master page is automatically applied to all translated versions. + </p> +</div> +</templates> diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml new file mode 100644 index 00000000..943a948e --- /dev/null +++ b/addons/website/static/src/xml/website.backend.xml @@ -0,0 +1,100 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <div t-name="WidgetWebsiteButton" class="o_stat_info published"> + <span class="o_stat_text o_value"/> + <span class="o_stat_text">On Website</span> + </div> + + <t t-name="WidgetWebsiteButtonIcon"> + <button type="button" class="btn oe_stat_button"> + <i class="fa fa-fw o_button_icon fa-globe"/> + <div class="o_stat_info"> + <span class="o_stat_text">Go to<br/>Website</span> + </div> + </button> + </t> + + <t t-name="website.WebsiteDashboardMain"> + <div class="o_dashboards"> + <div class="container-fluid o_website_dashboard"> + <t t-call="website.dashboard_header"/> + <t t-call="website.dashboard_content"/> + </div> + </div> + </t> + + <t t-name="website.dashboard_header"> + <div class="row o_dashboard_common"/> + </t> + + <t t-name="website.dashboard_content"> + <div class="o_website_dashboard_content"> + <t t-call="website.google_analytics_content"/> + </div> + </t> + <t t-name="website.google_analytics_content"> + <div class="row o_dashboard_visits" t-if="widget.groups.website_designer"> + <div class="col-12 o_box"> + <h2>Visits</h2> + <div t-if="widget.dashboards_data.visits && widget.dashboards_data.visits.ga_client_id"> + <div class="row js_analytics_components"/> + <a href="#" class="js_link_analytics_settings">Edit my Analytics Client ID</a> + </div> + <div t-if="!(widget.dashboards_data.visits && widget.dashboards_data.visits.ga_client_id)" class="col-lg-12"> + <div class="o_demo_background"> + <div class="o_layer"> + </div> + <div class="o_buttons text-center"> + <h3>There is no data currently available.</h3> + <button class="btn btn-primary js_link_analytics_settings d-block mx-auto mb8">Connect Google Analytics</button> + </div> + </div> + </div> + </div> + </div> + </t> + + <div t-name="website.unauthorized_analytics" class="col-12 js_unauthorized_message mb16"> + <span t-if="reason === 'not_connected'">You need to log in to your Google Account before: </span> + <span t-if="reason === 'no_right'">You do not seem to have access to this Analytics Account.</span> + <span t-if="reason === 'not_initialized'">Google Analytics initialization failed. Maybe this domain is not whitelisted in your Google Analytics project for this client ID.</span> + </div> + + <div t-name="website.ga_dialog_content"> + Your Tracking ID: <input type="text" name="ga_analytics_key" placeholder="UA-XXXXXXXX-Y" t-att-value="ga_analytics_key" style="width: 100%"></input> + <a href="https://www.odoo.com/documentation/14.0/applications/websites/website/optimize/google_analytics.html" target="_blank"> + <i class="fa fa-arrow-right"/> + How to get my Tracking ID + </a> + <br/><br/> + Your Client ID: <input type="text" name="ga_client_id" t-att-value="ga_key" style="width: 100%"></input> + <a href="https://www.odoo.com/documentation/14.0/applications/websites/website/optimize/google_analytics_dashboard.html" target="_blank"> + <i class="fa fa-arrow-right"/> + How to get my Client ID + </a> + </div> + + <t t-name="website.DateRangeButtons"> + <!-- TODO: Hide in mobile as it is going to push in control panel and it breaks UI, maybe we will improve it in future --> + <div class="btn-group o_date_range_buttons d-none d-md-inline-flex float-right"> + <button class="btn btn-secondary js_date_range active" data-date="week">Last Week</button> + <button class="btn btn-secondary js_date_range" data-date="month">Last Month</button> + <button class="btn btn-secondary js_date_range" data-date="year">Last Year</button> + </div> + <div class="btn-group d-none d-md-inline-block float-right" style="margin-right: 20px;"> + <t t-foreach="widget.websites" t-as="website"> + <button t-attf-class="btn btn-secondary js_website #{website.selected ? 'active' : ''}" + t-att-data-website-id="website.id"> + <t t-esc="website.name"/> + </button> + </t> + </div> + </t> + + <t t-name="website.GoToButtons"> + <a role="button" href="/" class="btn btn-primary" title="Go to Website"> + Go to Website + </a> + </t> + +</templates> diff --git a/addons/website/static/src/xml/website.background.video.xml b/addons/website/static/src/xml/website.background.video.xml new file mode 100644 index 00000000..18c1f4bd --- /dev/null +++ b/addons/website/static/src/xml/website.background.video.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-name="website.background.video"> + <div class="o_bg_video_container"> + <div class="o_bg_video_loading d-flex justify-content-center align-items-center text-primary"> + <div class="spinner-border" style="width: 4em; height: 4em;" role="status"> + <span class="sr-only">Loading...</span> + </div> + </div> + <iframe t-att-id="iframeID" + class="o_bg_video_iframe fade" + frameBorder="0" + t-att-src="videoSrc"/> + </div> + </t> +</templates> diff --git a/addons/website/static/src/xml/website.contentMenu.xml b/addons/website/static/src/xml/website.contentMenu.xml new file mode 100644 index 00000000..af318703 --- /dev/null +++ b/addons/website/static/src/xml/website.contentMenu.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> +<t t-name="website.contentMenu.dialog.submenu"> + <li t-att-data-menu-id="submenu.fields['id']" t-att-data-mega-menu="submenu.fields['is_mega_menu'] ? true : undefined"> + <div class="input-group"> + <div class="input-group-prepend"> + <span class="input-group-text fa fa-bars" role="img" aria-label="Dropdown menu" title="Dropdown menu"/> + </div> + <span class="form-control d-flex align-items-center"> + <span class="js_menu_label o_text_overflow flex-grow-1"> + <t t-esc="submenu.fields['name']"/> + </span> + <span t-if="submenu.fields['is_mega_menu']" class="badge badge-primary">Mega Menu</span> + <i t-if="submenu.is_homepage" class="fa fa-home ml-3" role="img" aria-label="Home" title="Home"/> + </span> + <span class="input-group-append"> + <button type="button" class="btn btn-primary js_edit_menu fa fa-pencil-square-o" aria-label="Edit Menu Item" title="Edit Menu Item"/> + <button type="button" class="btn btn-danger js_delete_menu fa fa-trash-o" aria-label="Delete Menu Item" title="Delete Menu Item"/> + </span> + </div> + <t t-set="children" t-value="submenu.children"/> + <ul t-if="children"> + <t t-foreach="children" t-as="submenu"> + <t t-call="website.contentMenu.dialog.submenu"/> + </t> + </ul> + </li> +</t> +<div t-name="website.contentMenu.dialog.select"> + <select class="form-control mb16" t-if="widget.roots"> + <t t-foreach="widget.roots" t-as="root"> + <option t-att-value="root.id"><t t-esc="root.name"/></option> + </t> + </select> +</div> +<div t-name="website.contentMenu.dialog.edit"> + <select class="form-control mb16" t-if="widget.roots"> + <t t-foreach="widget.roots" t-as="root"> + <option t-att-value="root.id"><t t-esc="root.name"/></option> + </t> + </select> + <ul class="oe_menu_editor list-unstyled"> + <t t-foreach="widget.menu.children" t-as="submenu"> + <t t-call="website.contentMenu.dialog.submenu"/> + </t> + </ul> + <div class="mt32"> + <small class="float-right text-muted"> + Drag to the right to get a submenu + </small> + <a href="#" class="js_add_menu"> + <i class="fa fa-plus-circle"/> Add Menu Item + </a><br/> + <a href="#" class="js_add_menu" data-type="mega"> + <i class="fa fa-plus-circle"/> Add Mega Menu Item + </a> + </div> +</div> +</templates> diff --git a/addons/website/static/src/xml/website.cookies_bar.xml b/addons/website/static/src/xml/website.cookies_bar.xml new file mode 100644 index 00000000..1f437869 --- /dev/null +++ b/addons/website/static/src/xml/website.cookies_bar.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="website.cookies_bar.text_title"> + <h3 class="o_cookies_bar_text_title"> + Respecting your privacy is our priority. + </h3> + </t> + <t t-name="website.cookies_bar.text_primary"> + <p class="o_cookies_bar_text_primary"> + We use cookies to provide you a better user experience. + </p> + </t> + <t t-name="website.cookies_bar.text_secondary"> + <p class="o_cookies_bar_text_secondary"> + We use them to store info about your habits on our website. It will helps us to provide you the very best experience and customize what you see. <br/> + By clicking on this banner, you give us permission to collect data. + </p> + </t> + <t t-name="website.cookies_bar.discrete"> + <section class="o_colored_level o_cc o_cc1"> + <div class="container"> + <div class="row"> + <div class="col-lg-8 pt16"> + <p>We use cookies to provide you a better user experience.</p> + </div> + <div class="col-lg-4 pt16 text-right"> + <a href="/cookie-policy" class="o_cookies_bar_text_policy btn btn-link btn-sm">Cookie Policy</a> + <a href="#" role="button" class="js_close_popup o_cookies_bar_text_button btn btn-primary btn-sm">I agree</a> + </div> + </div> + </div> + </section> + </t> + <t t-name="website.cookies_bar.classic"> + <section class="o_colored_level o_cc o_cc1 pt32 pb16"> + <div class="container"> + <div class="row"> + <div class="col-lg-6"> + <t t-call="website.cookies_bar.text_title"/> + <t t-call="website.cookies_bar.text_primary"/> + <t t-call="website.cookies_bar.text_secondary"/> + </div> + <div class="col-lg-3 offset-lg-3"> + <a href="#" role="button" class="js_close_popup o_cookies_bar_text_button btn btn-primary btn-block">I agree</a> + <a href="/cookie-policy" class="o_cookies_bar_text_policy btn btn-link btn-block">Cookie Policy</a> + </div> + </div> + </div> + </section> + </t> + <t t-name="website.cookies_bar.popup"> + <section class="o_colored_level o_cc o_cc1 p-5"> + <div class="container text-center"> + <div class="row"> + <div class="col-lg-12"> + <img t-attf-src="/web/image/website/#{websiteId}/logo/250x250" class="img img-fluid mb-4" alt="Website Logo"/> + <t t-call="website.cookies_bar.text_title"/> + <t t-call="website.cookies_bar.text_primary"/> + <t t-call="website.cookies_bar.text_secondary"/> + <a href="/cookie-policy" class=" o_cookies_bar_text_policy btn btn-link mr-2">Cookie Policy</a> + <a href="#" role="button" class="js_close_popup o_cookies_bar_text_button btn btn-primary">I agree</a> + </div> + </div> + </div> + </section> + </t> +</templates> diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml new file mode 100644 index 00000000..3ce515bd --- /dev/null +++ b/addons/website/static/src/xml/website.editor.xml @@ -0,0 +1,156 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <div t-name="website.homepage_editor_welcome_message" class="container text-center o_homepage_editor_welcome_message"> + <h2 class="mt0">Welcome to your <b>Homepage</b>!</h2> + <p class="lead d-none d-md-block">Click on <b>Edit</b> in the top right corner to start designing.</p> + </div> + <div t-name="website.leaving_current_page_edition"> + <p>What do you want to do?</p> + <p class="text-muted">Your current changes will be saved automatically.</p> + </div> + + <!-- Editor top bar which contains the summernote tools and save/discard buttons --> + <t t-name="website.editorbar"> + <div class="o_we_website_top_actions"> + <div class="o_we_external_history_buttons"> + <button type="button" data-action="undo" class="btn btn-secondary fa fa-undo"/> + <button type="button" data-action="redo" class="btn btn-secondary fa fa-repeat"/> + </div> + <form class="ml-auto"> + <button type="button" class="btn btn-secondary" data-action="cancel" accesskey="j">Discard</button> + <button type="button" class="btn btn-primary" data-action="save" accesskey="s">Save</button> + </form> + </div> + </t> + <!-- Custom checkbox (material-design-like toggle) --> + <t t-name="website.components.switch"> + <label class="o_switch" t-att-for="id"> + <input type="checkbox" t-att-id="id" t-att-checked="checked ? 'checked' : undefined"/> + <span/> + <div t-if="label"><t t-esc="label"/></div> + </label> + </t> + + <t t-extend="wysiwyg.widgets.link"> + <t t-jquery="#o_link_dialog_url_input" t-operation="after"> + <small class="form-text text-muted">Hint: Type '/' to search an existing page and '#' to link to an anchor.</small> + </t> + <t t-jquery="div#o_url_input" t-operation="after"> + <div class="form-group row o_link_dialog_page_anchor d-none"> + <label class="col-form-label col-md-3" for="o_link_dialog_anchor_input">Page Anchor</label> + <div class="col-md-9"> + <select name="link_anchor" class="form-control link-style"></select> + <small class="form-text font-weight-bold o_anchors_loading">Loading...</small> + </div> + </div> + </t> + </t> + <!-- Anchor Name option dialog --> + <div t-name="website.dialog.anchorName"> + <div class="form-group row"> + <label class="col-form-label col-md-3" for="anchorName">Choose an anchor name</label> + <div class="col-md-9"> + <input type="text" class="form-control o_input_anchor_name" id="anchorName" t-attf-value="#{currentAnchor}" placeholder="Anchor name"/> + <div class="invalid-feedback"> + <p class="d-none o_anchor_already_exists">The chosen name already exists</p> + </div> + </div> + </div> + </div> + + <!-- Add a Google Font option dialog --> + <div t-name="website.dialog.addGoogleFont"> + <div class="form-group row"> + <label class="col-form-label col-md-3" for="google_font_html">Google Font address</label> + <div class="col-md-9"> + <textarea id="google_font_html" class="form-control o_input_google_font" + placeholder="https://fonts.google.com/specimen/Roboto" style="height: 100px;"/> + <span class="float-right text-muted"> + Select one font on <a target="_blank" href="https://fonts.google.com">fonts.google.com</a> and copy paste the address of the font page here. + </span> + </div> + </div> + <p>Adding a font requires a reload of the page. This will save all your changes.</p> + </div> + <t t-name="website.delete_google_font_btn"> + <t t-set="delete_font_title">Delete this font</t> + <i role="button" + class="text-danger ml-2 fa fa-trash-o o_we_delete_google_font_btn" + t-att-aria-label="delete_font_title" + t-att-title="delete_font_title" + t-att-data-font-index="index"/> + </t> + <t t-name="website.add_google_font_btn"> + <we-button href="#" class="o_we_add_google_font_btn" + t-att-data-variable="variable"> + Add a Google Font + </we-button> + </t> + + <t t-name="website.color_combination_edition"> + <we-colorpicker string="Background" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-bg"/> + <we-colorpicker string="Text" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-text"/> + <we-collapse> + <we-colorpicker string="Headings" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-headings"/> + <we-colorpicker string="Headings 2" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-h2"/> + <we-colorpicker string="Headings 3" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-h3"/> + <we-colorpicker string="Headings 4" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-h4"/> + <we-colorpicker string="Headings 5" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-h5"/> + <we-colorpicker string="Headings 6" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-h6"/> + </we-collapse> + <we-colorpicker string="Links" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-link"/> + <we-row string="Primary Buttons"> + <we-colorpicker title="Background" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-btn-primary"/> + <we-colorpicker title="Border" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-btn-primary-border"/> + </we-row> + <we-row string="Secondary Buttons"> + <we-colorpicker title="Background" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-btn-secondary"/> + <we-colorpicker title="Border" data-customize-website-color="null" t-attf-data-color="o-cc#{number}-btn-secondary-border"/> + </we-row> + </t> + <div t-name="website.s_google_map_modal"> + <p>Use Google Map on your website (Contact Us page, snippets, etc).</p> + <div class="form-group row mb-0"> + <label class="col-sm-2 col-form-label" for="pin_address">API Key</label> + <div class="col"> + <div class="input-group"> + <div class="input-group-prepend"> + <div class="input-group-text"><i class="fa fa-key"/></div> + </div> + <input type="text" class="form-control" id="api_key_input" + t-att-value="apiKey or ''" + placeholder="BSgzTvR5L1GB9jriT451iTN4huVPxHmltG6T6eo"/> + </div> + <small id="api_key_help" class="text-danger"> + </small> + <div class="small form-text text-muted"> + Hint: How to use Google Map on your website (Contact Us page and as a snippet) + <br/> + <a target="_blank" href="https://console.developers.google.com/flows/enableapi?apiid=maps_backend,static_maps_backend&keyType=CLIENT_SIDE&reusekey=true"> + <i class="fa fa-arrow-right"/> + Create a Google Project and Get a Key + </a> + <br/> + <a target="_blank" href="https://cloud.google.com/maps-platform/pricing"> + <i class="fa fa-arrow-right"/> + Enable billing on your Google Project + </a> + </div> + </div> + </div> + </div> + + <!-- Theme - custom code --> + <div t-name="website.custom_code_dialog_content"> + <div class="mb-2" t-esc="contentText"/> + <div class="o_ace_editor_container"/> + </div> + + <t t-name="website.new_content_loader"> + <div class="o_new_content_loader_container position-fixed fixed-top fixed-left + h-100 w-100 d-flex flex-column align-items-center text-white font-weight-bold"> + <p id="new_content_loader_text"/> + <div class="o_new_content_loader"/> + </div> + </t> +</templates> diff --git a/addons/website/static/src/xml/website.pageProperties.xml b/addons/website/static/src/xml/website.pageProperties.xml new file mode 100644 index 00000000..5afdbb5f --- /dev/null +++ b/addons/website/static/src/xml/website.pageProperties.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + +<!-- Tooltip Dependencies --> + +<t t-name="website.get_tooltip_dependencies"> + <t t-foreach="dependencies" t-as="dep"> + <b><t t-esc="dep"/></b> + <ul> + <li t-foreach="dep_value" t-as="item"> + <a t-att-href="item_value['link']" + t-att-title="item_value['item']" + class="o_text_overflow"> + <t t-esc="item_value['item']"/> + </a> + </li> + </ul> + </t> +</t> +<t t-name="website.show_page_key_dependencies"> + <div class="col-md-9 offset-md-3"> + <span class="text-muted" id="warn_about_call_message"> + <t t-set="depTooltip"> + <t t-call="website.get_tooltip_dependencies"/> + </t> + It looks like your file is being called by + <a href="#" data-toggle="popover" t-att-data-content="depTooltip" data-html="true" title="Dependencies"><t t-esc="dep_text" /></a>. + Changing its name will break these calls. + </span> + </div> +</t> +<t t-name="website.show_page_dependencies"> + <t t-set="depTooltip"> + <t t-call="website.get_tooltip_dependencies"/> + </t> + (could be used in <a href="#" data-toggle="popover" t-att-data-content="depTooltip" data-html="true" title="Dependencies"><t t-esc="dep_text" /></a>) +</t> + +<!-- Page Properties --> + +<div t-name="website.pagesMenu.page_info" class="o_page_management_info"> + <form> + <ul class="nav nav-tabs" role="tablist"> + <li class="nav-item"><a aria-controls="basic_page_info" role="tab" data-toggle="tab" class="nav-link active" href="#basic_page_info">Name</a></li> + <li class="nav-item"><a aria-controls="advances_page_info" role="tab" data-toggle="tab" class="nav-link" href="#advances_page_info">Publish</a></li> + </ul> + <div class="tab-content mt16"> + <div role="tabpanel" id="basic_page_info" class="tab-pane fade show active"> + <div class="form-group row"> + <label class="col-form-label col-md-3" for="page_name">Page Name</label> + <div class="col-md-9"> + <input type="text" class="form-control" id="page_name" t-att-value="widget.page.name" /> + </div> + </div> + <div class="form-group warn_about_call"></div> + <div class="form-group row"> + <label class="col-form-label col-md-3" for="page_url">Page URL</label> + <div class="col-md-9"> + <div class="input-group"> + <div class="input-group-prepend"> + <span class="input-group-text" t-att-title="widget.serverUrl"><small><t t-esc="widget.serverUrlTrunc"/></small></span> + </div> + <input type="text" class="form-control" id="page_url" t-att-value="widget.page.url" /> + </div> + </div> + </div> + <div class="form-group row ask_for_redirect"> + <label class="col-form-label col-md-3" for="create_redirect">Redirect Old URL</label> + <div class="col-md-2"> + <a> + <label class="o_switch" for="create_redirect" > + <input type="checkbox" id="create_redirect"/> + <span/> + </label> + </a> + </div> + <div class="col-md-7 mt4"> + <span class="text-muted" id="dependencies_redirect"></span> + </div> + </div> + <div class="form-group row ask_for_redirect"> + <label class="col-form-label col-md-3 redirect_type" for="redirect_type">Type</label> + <div class="col-md-6 redirect_type"> + <select class="form-control" id="redirect_type"> + <option value="301">301 Moved permanently</option> + <option value="302">302 Moved temporarily</option> + </select> + </div> + </div> + </div> + <div role="tabpanel" id="advances_page_info" class="tab-pane fade"> + <div class="form-group row"> + <label class="control-label col-md-4" for="is_menu">Show in Top Menu</label> + <div class="col-sm-8"> + <label class="o_switch" for="is_menu" > + <input type="checkbox" t-att-checked="widget.page.menu_ids.length > 0 ? true : undefined" id="is_menu"/> + <span/> + </label> + </div> + </div> + <div class="form-group row"> + <label class="control-label col-md-4" for="is_homepage">Use as Homepage</label> + <div class="col-sm-8"> + <label class="o_switch" for="is_homepage" > + <input type="checkbox" t-att-checked="widget.page.is_homepage ? true : undefined" id="is_homepage"/> + <span/> + </label> + </div> + </div> + <div class="form-group row"> + <label class="control-label col-md-4" for="is_indexed"> + Indexed + <i class="fa fa-question-circle-o" title="Hide this page from search results" role="img" aria-label="Info"></i> + </label> + <div class="col-md-2"> + <label class="o_switch" for="is_indexed" > + <input type="checkbox" t-att-checked="widget.page.website_indexed ? true : undefined" id="is_indexed"/> + <span/> + </label> + </div> + </div> + <div class="form-group row"> + <label class="control-label col-md-4" for="is_published">Publish</label> + <div class="col-sm-8"> + <label class="o_switch js_publish_btn" for="is_published"> + <input type="checkbox" t-att-checked="widget.page.website_published ? true : undefined" id="is_published"/> + <span/> + </label> + </div> + </div> + <div class="form-group row"> + <label class="control-label col-md-4" for="date_publish">Publishing Date</label> + <div class="col-md-8"> + <div class="input-group date" id="date_publish_container" data-target-input="nearest"> + <input type="text" class="form-control datetimepicker-input" data-target="#date_publish_container" id="date_publish"/> + <div class="input-group-append" data-target="#date_publish_container" data-toggle="datetimepicker"> + <div class="input-group-text"><i class="fa fa-calendar"></i></div> + </div> + </div> + </div> + </div> + <div class="form-group row"> + <label class="control-label col-md-4" for="visibility">Visibility</label> + <div class="col-md-8"> + <div class="input-group"> + <select id="visibility" class="form-control col-md-4"> + <option value="" t-att-selected="widget.page.visibility == ''">Public</option> + <option value="connected" t-att-selected="widget.page.visibility == 'connected' ? 'selected' : undefined">Signed In</option> + <option value="restricted_group" t-att-selected="widget.page.visibility == 'restricted_group' ? 'selected' : undefined">Some Users</option> + <option value="password" t-att-selected="widget.page.visibility == 'password' ? 'selected' : undefined">Password</option> + </select> + <div class="ml-1 input-group-prepend show_visibility_password" > + <div class="input-group-text"><i class="fa fa-key"></i></div> + </div> + <input type="password" id="visibility_password" + t-att-value='widget.page.visibility_password' + t-att-required="widget.page.visibility == 'password' ? 'required' : None" + class="form-control show_visibility_password"/> + <t t-if="widget.page.hasSingleGroup"> + <div class="ml-1 input-group-prepend show_group_id"> + <div class="input-group-text"><i class="fa fa-group"></i></div> + </div> + <t t-set="group" t-value="widget.page.group_id"/> + <input type="text" class="form-control show_group_id" id="group_id" t-att-value="group ? group[1] : ''" t-att-data-group-id='group ? group[0] : 0' /> + </t> + <t t-else=""> + <t t-set='groups_tooltip'>More than one group has been set on the view.</t> + <a class="show_group_id btn btn-link mx-auto" href="/web#model=ir.ui.view&id=681" t-att-title='groups_tooltip'>Discard & Edit in backend</a> + </t> + </div> + </div> + </div> + </div> + </div> + </form> +</div> +</templates> diff --git a/addons/website/static/src/xml/website.res_config_settings.xml b/addons/website/static/src/xml/website.res_config_settings.xml new file mode 100644 index 00000000..6b6d4817 --- /dev/null +++ b/addons/website/static/src/xml/website.res_config_settings.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> +<t t-name="website.res_config_settings.cookies_modal_main"> + <main role="alert"> + <p> + <b>Cookie bars may significantly impair the experience</b> of + your visitors. We recommend you avoid them unless you have + verified with a legal advisor that you absolutely need cookie + consent in your country. + </p> + <p> + For session cookies, authentification and analytics*, + <b>you do not need to ask for the consent</b> (see e.g. Opinion + 04/2012 on Cookie Consent Exemption by the EU Art.29 WP). + </p> + <p> + * provided that your analytics is anonymized, which is not the + case by default with Google Analytics. + </p> + </main> +</t> +</templates> diff --git a/addons/website/static/src/xml/website.seo.xml b/addons/website/static/src/xml/website.seo.xml new file mode 100644 index 00000000..b10faaff --- /dev/null +++ b/addons/website/static/src/xml/website.seo.xml @@ -0,0 +1,168 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="Configurator.language_promote"> + <t t-foreach="language" t-as="lang"> + <option t-att-value="lang[0]" t-att-selected="lang[0] == def_lang ? 'selected' : null"><t t-esc="lang[2]" /></option> + </t> + </t> + + <div t-name="website.seo_configuration" role="form"> + <section class="js_seo_meta_title_description"/> + <section class="js_seo_meta_keywords"/> + <section class="js_seo_image"/> + </div> + + <t t-name="website.seo_suggestion_list"> + <ul class="list-inline mb0"> + <!-- filled in JS --> + </ul> + </t> + + <t t-name="website.seo_list"> + <tbody> + <!-- filled in JS --> + </tbody> + </t> + + <t t-name="website.seo_keyword"> + <tr class="js_seo_keyword" t-att-data-keyword="widget.keyword"> + <td t-esc="widget.keyword"/> + <td class="text-center"><i t-if="widget.used_h1" class="fa fa-check" t-attf-title="{{ widget.keyword }} is used in page first level heading"/></td> + <td class="text-center"><i t-if="widget.used_h2" class="fa fa-check" t-attf-title="{{ widget.keyword }} is used in page second level heading"/></td> + <td class="text-center"><i class="js_seo_keyword_title fa fa-check" style="visibility: hidden;" t-attf-title="{{ widget.keyword }} is used in page title"/></td> + <td class="text-center"><i class="js_seo_keyword_description fa fa-check" style="visibility: hidden;" t-attf-title="{{ widget.keyword }} is used in page description"/></td> + <td class="text-center"><i t-if="widget.used_content" class="fa fa-check" t-attf-title="{{ widget.keyword }} is used in page content"/></td> + <td class="o_seo_keyword_suggestion"/> + <td class="text-center"><a href="#" class="oe_remove" data-action="remove-keyword" t-attf-title="Remove {{ widget.keyword }}"><i class="fa fa-trash"/></a></td> + </tr> + </t> + + <t t-name="website.seo_suggestion"> + <li class="list-inline-item"> + <span class="o_seo_suggestion badge badge-info" t-att-data-keyword="widget.keyword" t-attf-title="Add {{ widget.keyword }}" t-esc="widget.keyword"/> + </li> + </t> + + <t t-name="website.seo_preview"> + <div class="oe_seo_preview_g"> + <div class="rc"> + <div class="r"><t t-esc="widget.title"/></div> + <div class="s"> + <div class="kv"><t t-esc="widget.url"/></div> + <div class="st"><t t-esc="widget.description"/></div> + </div> + </div> + </div> + </t> + + <div t-name="website.seo_meta_title_description"> + <div class="row"> + <div class="col-lg-6"> + <div class="form-group"> + <label for="website_meta_title"> + Title <i class="fa fa-question-circle-o" title="The title will take a default value unless you specify one."/> + </label> + <input type="text" name="website_meta_title" id="website_meta_title" class="form-control" placeholder="Keep empty to use default value" maxlength="70" size="70"/> + </div> + <div class="form-group"> + <label for="website_meta_description"> + Description <i class="fa fa-question-circle-o" t-att-title="widget.previewDescription"/> + </label> + <textarea name="website_meta_description" id="website_meta_description" placeholder="Keep empty to use default value" class="form-control"/> + <div class="alert alert-warning mt16 mb0 small" id="website_meta_description_warning" style="display: none;"/> + </div> + <div class="form-group" t-if='widget.canEditUrl'> + <label for="website_seo_name"> + Custom Url <i class="fa fa-question-circle-o" t-att-title="widget.seoNameHelp" /> + </label> + <div class="input-group"> + <div class="input-group-prepend"> + <span class="input-group-text seo_name_pre"></span> + </div> + <input type="text" name="website_seo_name" id="website_seo_name" class="form-control" t-att-placeholder="widget.seoNameDefault" /> + <div class="input-group-append" title="Unalterable unique identifier"> + <span class="input-group-text seo_name_post"></span> + </div> + </div> + </div> + </div> + <div class="col-lg-6"> + <div class="card-header">Preview</div> + <div class="card mb0 p-0"> + <div class="card-body"> + <div class="js_seo_preview"/> + </div> + </div> + </div> + </div> + </div> + + <t t-name="website.seo_meta_keywords"> + <label for="website_meta_keywords"> + Keywords + </label> + <div class="form-inline" role="form"> + <div class="input-group"> + <input type="text" name="website_meta_keywords" id="website_meta_keywords" class="form-control" placeholder="Keyword" maxlength="30"/> + <span title="The language of the keyword and related keywords." class="input-group-append"> + <select name="seo_page_language" id="language-box" class="btn form-control"/> + </span> + <span class="input-group-append"> + <button data-action="add" class="btn btn-primary btn-sm" type="button">Add</button> + </span> + </div> + </div> + <div class="table-responsive mt16"> + <table class="table table-sm"> + <thead> + <tr> + <th>Keyword</th> + <th class="text-center" title="Used in page first level heading">H1</th> + <th class="text-center" title="Used in page second level heading">H2</th> + <th class="text-center" title="Used in page title">T</th> + <th class="text-center" title="Used in page description">D</th> + <th class="text-center" title="Used in page content">C</th> + <th title="Most searched topics related to your keyword, ordered by importance">Related keywords</th> + <th class="text-center"></th> + </tr> + </thead> + <!-- body inserted in JS --> + </table> + </div> + </t> + + <div t-name="website.seo_meta_image_selector" class="o_seo_og_image"> + <t t-call="website.og_image_body"/> + </div> + + <t t-name="website.og_image_body"> + <h4><small>Select an image for social share</small></h4> + <div class="row"> + <div class="col-lg-6"> + <t t-foreach="widget.images" t-as="image"> + <div t-attf-class="o_meta_img mt4 #{new window.URL(image, window.location.origin).pathname === new window.URL(widget.activeMetaImg, window.location.origin).pathname and ' o_active_image' or ''}"> + <img t-att-src="image"/> + </div> + </t> + <div t-if="widget.customImgUrl" t-attf-class="o_meta_img mt4 #{widget.customImgUrl === widget.activeMetaImg and ' o_active_image' or ''}"> + <span class="o-custom-label w-100 text-white text-center">Custom</span> + <img t-att-src="widget.customImgUrl"/> + </div> + <div class="o_meta_img_upload mt4" title="Click to choose more images"> + <i class="fa fa-upload"/> + </div> + </div> + <div class="col-lg-6"> + <div class="card p-0 mb16"> + <div class="card-header">Social Preview</div> + <img class="card-img-top o_meta_active_img" t-att-src="widget.activeMetaImg"/> + <div class="card-body px-3 py-2"> + <h6 class="text-primary card-title mb0"><t t-esc="widget.metaTitle"/></h6> + <small class="card-subtitle text-muted"><t t-esc="widget.serverUrl"/></small> + <p t-esc="widget.metaDescription"/> + </div> + </div> + </div> + </div> + </t> +</templates> diff --git a/addons/website/static/src/xml/website.share.xml b/addons/website/static/src/xml/website.share.xml new file mode 100644 index 00000000..f07a2907 --- /dev/null +++ b/addons/website/static/src/xml/website.share.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="website.social_hover"> + <div class="text-nowrap css_editable_mode_hidden"> + <t t-foreach="medias" t-as="media"> + <a href="#" + t-attf-class="fa fa-3x fa-#{media}-square text-#{media} oe_social_#{media}" + t-att-title="media" + t-att-aria-label="media"/> + </t> + </div> + </t> +</templates> diff --git a/addons/website/static/src/xml/website.xml b/addons/website/static/src/xml/website.xml new file mode 100644 index 00000000..8d196b9b --- /dev/null +++ b/addons/website/static/src/xml/website.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="website.prompt"> + <div role="dialog" class="modal o_technical_modal" tabindex="-1"> + <div class="modal-dialog"> + <div class="modal-content"> + <header class="modal-header" t-if="window_title"> + <h3 class="modal-title"><t t-esc="window_title"/></h3> + <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button> + </header> + <main class="modal-body"> + <form role="form" t-att-id="id"> + <div class="form-group row mb0"> + <label for="page-name" class="col-md-3 col-form-label"> + <t t-esc="field_name"/>: + </label> + <div class="col-md-9"> + <input t-if="field_type == 'input'" type="text" class="form-control" required="required"/> + <textarea t-if="field_type == 'textarea'" class="form-control" required="required" rows="5"></textarea> + <select t-if="field_type == 'select'" class="form-control"></select> + </div> + </div> + </form> + </main> + <footer class="modal-footer"> + <button type="button" class="btn btn-primary btn-continue">Continue</button> + <button type="button" class="btn btn-secondary" data-dismiss="modal" aria-label="Cancel">Cancel</button> + </footer> + </div> + </div> + </div> + </t> + + <t t-name="website.dependencies"> + <p class="text-warning">Don't forget to update all links referring to this page.</p> + <t t-if="dependencies and _.keys(dependencies).length"> + <p class="text-warning">We found these ones:</p> + <div t-foreach="dependencies" t-as="type" class="mb16"> + <a class="collapsed fa fa-caret-right" data-toggle="collapse" t-attf-href="#collapseDependencies#{type_index}" aria-expanded="false" t-attf-aria-controls="collapseDependencies#{type_index}"> + <t t-esc="type"/>&nbsp; + <span class="text-muted"><t t-esc="type_value.length"/> found(s)</span> + </a> + <div t-attf-id="collapseDependencies#{type_index}" class="collapse" aria-expanded="false"> + <ul> + <li t-foreach="type_value" t-as="error"> + <a t-if="!_.contains(['', '#', false], error.link)" t-att-href="error.link"> + <t t-raw="error.text"/> + </a> + <t t-else=""> + <t t-raw="error.text"/> + </t> + </li> + </ul> + </div> + </div> + </t> + </t> + + <div t-name="website.delete_page"> + <p>Are you sure you want to delete this page ?</p> + <t t-call="website.dependencies"/> + </div> + + <div t-name="website.rename_page"> + <div class="card"> + <div class="card-body"> + <form> + <div class="form-group row mb0"> + <label for="new_name" class="col-form-label col-md-4">Rename Page To:</label> + <div class="col-md-8"> + <input type="text" class="form-control" id="new_name" placeholder="e.g. About Us"/> + </div> + </div> + </form> + </div> + </div> + <t t-call="website.dependencies"/> + </div> + + <div t-name="website.duplicate_page_action_dialog"> + <div class="form-group row"> + <label class="col-form-label col-md-3" for="page_name">Page Name</label> + <div class="col-md-9"> + <input type="text" class="form-control" id="page_name"/> + </div> + </div> + </div> + + <t t-name="website.oe_applications_menu"> + <t t-as="menu" t-foreach="menu_data.children"> + <a role="menuitem" class="dropdown-item" + t-att-data-action-id="menu.action ? menu.action.split(',')[1] : undefined" + t-att-data-action-model="menu.action ? menu.action.split(',')[0] : undefined" + t-att-data-menu="menu.id" + t-att-data-menu-xmlid="menu.xmlid" + t-att-href="_.str.sprintf('/web#menu_id=%s&action=%s', menu.id, menu.action ? menu.action.split(',')[1] : '')"> + <span class="oe_menu_text" t-esc="menu.name"/> + </a> + </t> + </t> +</templates> diff --git a/addons/website/static/src/xml/website_widget.xml b/addons/website/static/src/xml/website_widget.xml new file mode 100644 index 00000000..48a6d7c5 --- /dev/null +++ b/addons/website/static/src/xml/website_widget.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates xml:space="preserve"> + + <t t-name="website.iframeWidget"> + <iframe + t-if="url" + height="100%" + width="100%" + frameBorder="0" + t-att-src="url" + class='d-block' + ></iframe> + <div t-else="">No Url</div> + </t> + +</templates> |
