├── .babelrc
├── .github
├── FUNDING.yml
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── LICENSE
├── README.md
├── TODO.md
├── dist
├── grapesjs-blocks-bootstrap4.min.js
├── grapesjs-blocks-bootstrap4.min.js.LICENSE.txt
└── index.html
├── index.html
├── package-lock.json
├── package.json
├── src
├── bootstrap-btn-sizes.js
├── bootstrap-contexts.js
├── commands.js
├── components.js
├── components
│ ├── Alert.js
│ ├── Badge.js
│ ├── Button.js
│ ├── ButtonGroup.js
│ ├── ButtonToolbar.js
│ ├── Card.js
│ ├── Checkbox.js
│ ├── Collapse.js
│ ├── Column.js
│ ├── ColumnBreak.js
│ ├── Container.js
│ ├── Default.js
│ ├── Dropdown.js
│ ├── FileInput.js
│ ├── Form.js
│ ├── Header.js
│ ├── Image.js
│ ├── Input.js
│ ├── InputGroup.js
│ ├── Label.js
│ ├── Link.js
│ ├── List.js
│ ├── MediaObject.js
│ ├── Paragraph.js
│ ├── Radio.js
│ ├── Row.js
│ ├── Select.js
│ ├── Text.js
│ ├── Textarea.js
│ ├── tabs
│ │ ├── Tab.js
│ │ ├── TabPane.js
│ │ ├── TabsNavigation.js
│ │ ├── TabsPanes.js
│ │ └── constants.js
│ └── video
│ │ ├── Embed.js
│ │ └── Video.js
├── devices.js
├── icons
│ ├── button.svg
│ ├── caret-square-down-regular.svg
│ ├── certificate-solid.svg
│ ├── check-square-solid.svg
│ ├── circle-solid.svg
│ ├── columns-solid.svg
│ ├── compress-solid.svg
│ ├── credit-card-solid.svg
│ ├── dot-circle-regular.svg
│ ├── ellipsis-h-solid.svg
│ ├── equals-solid.svg
│ ├── exclamation-triangle-solid.svg
│ ├── file-input.svg
│ ├── font-solid.svg
│ ├── form-group.svg
│ ├── form.svg
│ ├── heading-solid.svg
│ ├── image-light.svg
│ ├── image-solid.svg
│ ├── input-group.svg
│ ├── input.svg
│ ├── label.svg
│ ├── link-solid.svg
│ ├── paragraph-solid.svg
│ ├── select-input.svg
│ ├── textarea.svg
│ ├── window-maximize-solid.svg
│ └── youtube-brands.svg
├── index.js
├── traits.js
└── utils.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ],
5 | "plugins": [
6 | "@babel/plugin-proposal-object-rest-spread"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: kaoz70
4 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [master, ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [master]
9 | schedule:
10 | - cron: '0 23 * * 5'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v2
20 | with:
21 | # We must fetch at least the immediate parents so that if this is
22 | # a pull request then we can checkout the head.
23 | fetch-depth: 2
24 |
25 | # If this run was triggered by a pull request event, then checkout
26 | # the head of the pull request instead of the merge commit.
27 | - run: git checkout HEAD^2
28 | if: ${{ github.event_name == 'pull_request' }}
29 |
30 | # Initializes the CodeQL tools for scanning.
31 | - name: Initialize CodeQL
32 | uses: github/codeql-action/init@v1
33 | # Override language selection by uncommenting this and choosing your languages
34 | # with:
35 | # languages: go, javascript, csharp, python, cpp, java
36 |
37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
38 | # If this step fails, then you should remove it and run the build manually (see below)
39 | - name: Autobuild
40 | uses: github/codeql-action/autobuild@v1
41 |
42 | # ℹ️ Command-line programs to run using the OS shell.
43 | # 📚 https://git.io/JvXDl
44 |
45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
46 | # and modify them (or add more) to build your code if your project
47 | # uses a compiled language
48 |
49 | #- run: |
50 | # make bootstrap
51 | # make release
52 |
53 | - name: Perform CodeQL Analysis
54 | uses: github/codeql-action/analyze@v1
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | private/
3 | node_modules/
4 | .eslintrc
5 | *.log
6 | _index.html
7 | .idea
8 |
9 | *~
10 | *.swp
11 | *.swo
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, (YOUR NAME)
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | - Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 | - Redistributions in binary form must reproduce the above copyright notice, this
10 | list of conditions and the following disclaimer in the documentation and/or
11 | other materials provided with the distribution.
12 | - Neither the name "GrapesJS" nor the names of its contributors may be
13 | used to endorse or promote products derived from this software without
14 | specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GrapesJS Bootstrap v4 Blocks Plugin
2 |
3 | [](https://www.npmjs.com/package/grapesjs-blocks-bootstrap4)
4 |
5 |
6 |
7 |
10 |
11 | ### Bootstrap 5
12 | For a Bootstrap 5 version, you can use the [Bootstrap 5 plugin](https://github.com/MoonriseSoftwareCalifornia/grapesjs-blocks-bootstrap5) from [Eric Dean Kauffman](https://github.com/toiyabe) which is a fork of this project.
13 |
14 | ## Summary
15 |
16 | * Plugin name: `grapesjs-blocks-bootstrap4`
17 | * Components (see Options for list of Blocks)
18 | * Layout
19 | * `container`
20 | * `row`
21 | * `column`
22 | * `column_break`
23 | * `media_object`
24 | * `media_body`
25 | * Components
26 | * `alert`
27 | * `tabs`
28 | * `badge`
29 | * `card`
30 | * `card_container`
31 | * `collapse`
32 | * `dropdown`
33 | * `dropdown_menu`
34 | * Typography
35 | * `text`
36 | * `header`
37 | * `paragraph`
38 | * Media
39 | * `image`
40 | * `video`
41 | * Forms
42 | * `form`
43 | * `button`
44 | * `button_group`
45 | * `button_toolbar`
46 | * `input`
47 | * `input_group`
48 | * `form_group_input`
49 | * `textarea`
50 | * `checkbox`
51 | * `radio`
52 |
53 |
54 |
55 |
56 |
57 | ## Options
58 |
59 | ```js
60 | {
61 | blocks: {
62 | ...
63 | }
64 | blockCategories: {
65 | ...
66 | }
67 | labels: {
68 | ...
69 | }
70 | formPredefinedActions: null,
71 | optionsStringSeparator: '::'
72 | }
73 | ```
74 |
75 | ### Blocks
76 |
77 | |Option|Description|Default|
78 | |-|-|-
79 | |`default`|Rebuild default component with utility settings|true|
80 | |`text`|Rebuild text component to re-inherit from default|true|
81 | |`link`|Rebuild link component to re-inherit from default and give toggle setting|true|
82 | |`image`|Rebuild image component to re-inherit from default|true|
83 | |`video`|Rebuild video component to re-inherit from default|true|
84 | |`container`|Container (fixed/fluid)|true|
85 | |`row`|Row|true|
86 | |`column`|Columns of all sizes|true|
87 | |`column_break`|Column-break (`div.w-100`)|true|
88 | |`media_object`|Media object|true|
89 | |`alert`||true|
90 | |`tabs`||true|
91 | |`badge`||true|
92 | |`card`|Card with settings for images, image overlay, header, body, & footer components|true|
93 | |`card_container`|Layouts: group, deck, columns|true|
94 | |`collapse`|Collapse component that can be toggled via link component|true|
95 | |`dropdown`|Dropdown|true|
96 | |`header`|H1-H6|true|
97 | |`paragraph`|P tag with "lead" setting|true|
98 | |`form`||true|
99 | |`button`||true|
100 | |`button_group`||true|
101 | |`button_toolbar`||true|
102 | |`input`||true|
103 | |`input_group`||true|
104 | |`form_group_input`||true|
105 | |`textarea`||true|
106 | |`checkbox`||true|
107 | |`radio`||true|
108 |
109 | ### Block Categories
110 |
111 | These are the different categories of blocks as they are grouped in the Blocks sidebar panel. Set a value to false exclude entire groups of blocks (as well as the associated components).
112 |
113 | |Option|Description|Default|
114 | |-|-|-
115 | |`layout`|Container, row, col, col-break, media object|true|
116 | |`components`|_Bootstrap_'s Components--alert, button, card, etc.|true|
117 | |`typography`|Text, header, paragraph, etc.|true|
118 | |`basic`|Link, image, etc.|true|
119 | |`forms`|Form, input, textarea, etc.|true|
120 |
121 |
122 | ### Labels
123 |
124 | Same keys as Blocks, but value is the label for the block.
125 |
126 | |Option|Description|Default|
127 | |-|-|-
128 | |`text`||'Text'|
129 | |`header`||'Header'|
130 |
131 | etc.
132 |
133 | ### Other
134 |
135 | |Option|Description|Default|
136 | |-|-|-
137 | |`gridDevices`|Add devices based on BS grid breakpoints|true|
138 | |`gridDevicesPanel`|Build a panel in the top-left corner with device buttons (use with editor `showDevices`=`false`)|false|
139 | |`formPredefinedActions`|Pass a list of predefined form actions to generate a select menu: [{name: 'Contact', value: '/contact'}, ...], if no list is passed an input box to add the action is shown|null|
140 | |`optionsStringSeparator`|Pass a string to identify the separator of values and labels of the select options: optionValue::optionLabel. This setting WILL BE overridden by the gjs-preset-webpage plugin if enabled|'::'|
141 |
142 |
143 | ## Download
144 |
145 |
147 | * NPM
148 | * `npm i grapesjs-blocks-bootstrap4`
149 | * GIT
150 | * `git clone https://github.com/kaoz70/grapesjs-blocks-bootstrap4.git`
151 |
152 |
153 |
154 |
155 |
156 | ## Usage
157 |
158 | ```html
159 |
160 |
161 |
162 |
163 |
164 |
165 |
196 | ```
197 |
198 |
199 |
200 |
201 |
202 | ## Development
203 |
204 | Clone the repository
205 |
206 | ```sh
207 | $ git clone https://github.com/kaoz70/grapesjs-blocks-bootstrap4.git
208 | $ cd grapesjs-blocks-bootstrap4
209 | ```
210 |
211 | Install dependencies
212 |
213 | ```sh
214 | $ npm i
215 | ```
216 |
217 | The plugin relies on GrapesJS via `peerDependencies` so you have to install it manually (without adding it to package.json)
218 |
219 | ```sh
220 | $ npm i grapesjs --no-save
221 | ```
222 |
223 | Start the dev server
224 |
225 | ```sh
226 | $ npm start
227 | ```
228 |
229 |
230 |
231 |
232 |
233 | ## License
234 |
235 | BSD 3-Clause
236 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | - flex container component (row will extend this)
2 | - flex item component (column will extend this)
3 | - breadcrumb component
4 | - buttons js plugin (http://getbootstrap.com/docs/4.0/components/buttons/#button-plugin)
5 | - carousel
6 | - automatic ARIA data attrs for link/collapse
7 | - they go on the link, but we need to lookup the collapse with id=link.href to check its "show" state
8 | - search through the components and find the one with that id
9 | - class_select trait needs to somehow listen to changes of classes on selected component and re-render
10 |
--------------------------------------------------------------------------------
/dist/grapesjs-blocks-bootstrap4.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*! grapesjs-blocks-bootstrap4 - 0.2.5 */
2 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | GrapesJS Bootstrap v4 Blocks Plugin
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
35 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
Hello!
48 |
This is demo content from index.html
. For development, you shouldn't edit this file. Instead, you can copy and rename it to _index.html
. The next time the server starts, the new file will be served, and it will be ignored by git.
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
84 |
85 |
86 |
87 |
Home
88 |
Profile
89 |
Messages
90 |
Settings
91 |
92 |
93 |
94 |
95 | VIDEO
96 |
97 |
98 |
99 |
100 |
101 |
102 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | GrapesJS Bootstrap v4 Blocks Plugin
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
35 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
Hello!
48 |
This is demo content from index.html
. For development, you shouldn't edit this file. Instead, you can copy and rename it to _index.html
. The next time the server starts, the new file will be served, and it will be ignored by git.
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
84 |
85 |
86 |
87 |
Home
88 |
Profile
89 |
Messages
90 |
Settings
91 |
92 |
93 |
94 |
95 | VIDEO
96 |
97 |
98 |
99 |
100 |
101 |
102 |
137 |
138 |
139 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grapesjs-blocks-bootstrap4",
3 | "version": "0.2.5",
4 | "description": "GrapesJS Bootstrap v4 Blocks Plugin",
5 | "main": "dist/grapesjs-blocks-bootstrap4.min.js",
6 | "scripts": {
7 | "dev": "webpack serve --mode development --progress --env development",
8 | "lint": "eslint src",
9 | "v:patch": "npm version --no-git-tag-version patch",
10 | "v:minor": "npm version --no-git-tag-version minor",
11 | "build": "npm run v:patch && webpack --env production",
12 | "build-dev": "npm run && webpack --env production",
13 | "start": "webpack-cli serve --mode development --progress"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/kaoz70/grapesjs-bootstrap4-blocks.git"
18 | },
19 | "keywords": [
20 | "grapesjs",
21 | "plugin",
22 | "bootstrap"
23 | ],
24 | "author": "z1lk",
25 | "license": "BSD-3-Clause",
26 | "peerDependencies": {
27 | "grapesjs": "0.x"
28 | },
29 | "devDependencies": {
30 | "@babel/core": "^7.12.17",
31 | "@babel/plugin-proposal-object-rest-spread": "^7.13.0",
32 | "@babel/preset-env": "^7.13.5",
33 | "babel-loader": "^8.2.2",
34 | "eslint": "^7.20.0",
35 | "html-webpack-plugin": "^5.6.3",
36 | "raw-loader": "^4.0.2",
37 | "terser-webpack-plugin": "^5.3.10",
38 | "webpack": "^5.96.1",
39 | "webpack-cli": "^5.1.4",
40 | "webpack-dev-server": "^5.1.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/bootstrap-btn-sizes.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'lg': 'Large',
3 | 'sm': 'Small'
4 | };
5 |
--------------------------------------------------------------------------------
/src/bootstrap-contexts.js:
--------------------------------------------------------------------------------
1 | export default [
2 | 'primary',
3 | 'secondary',
4 | 'success',
5 | 'info',
6 | 'warning',
7 | 'danger',
8 | 'light',
9 | 'dark',
10 | ];
11 |
--------------------------------------------------------------------------------
/src/commands.js:
--------------------------------------------------------------------------------
1 | export default (editor, config = {}) => {
2 | const commands = editor.Commands;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components.js:
--------------------------------------------------------------------------------
1 | import Collapse, {CollapseBlock} from './components/Collapse';
2 | import Dropdown, {DropDownBlock} from './components/Dropdown';
3 | import TabsNavigation, {TabsBlock} from "./components/tabs/TabsNavigation";
4 | import TabsPanes from "./components/tabs/TabsPanes";
5 | import Tab from "./components/tabs/Tab";
6 | import TabPane from "./components/tabs/TabPane";
7 | import Form, {FormBlock} from "./components/Form";
8 | import Input, {InputBlock} from "./components/Input";
9 | import InputGroup, {InputGroupBlock} from "./components/InputGroup";
10 | import Textarea, {TextareaBlock} from "./components/Textarea";
11 | import Select, {SelectBlock} from "./components/Select";
12 | import Checkbox, {CheckboxBlock} from "./components/Checkbox";
13 | import Radio, {RadioBlock} from "./components/Radio";
14 | import Button, {ButtonBlock} from "./components/Button";
15 | import ButtonGroup, {ButtonGroupBlock} from "./components/ButtonGroup";
16 | import ButtonToolbar, {ButtonToolbarBlock} from "./components/ButtonToolbar";
17 | import Label, {LabelBlock} from "./components/Label";
18 | import Link, {LinkBlock} from "./components/Link";
19 | import FileInput, {FileInputBlock} from "./components/FileInput";
20 | import Image, {ImageBlock} from "./components/Image";
21 | import Video, {VideoBlock} from "./components/video/Video";
22 | import Embed from "./components/video/Embed";
23 | import Paragraph, {ParagraphBlock} from "./components/Paragraph";
24 | import Header, {HeaderBlock} from "./components/Header";
25 | import Card, {CardBlock} from "./components/Card";
26 | import Badge, {BadgeBlock} from "./components/Badge";
27 | import Alert, {AlertBlock} from "./components/Alert";
28 | import MediaObject, {MediaObjectBlock} from "./components/MediaObject";
29 | import ColumnBreak, {ColumnBreakBlock} from "./components/ColumnBreak";
30 | import Column, {ColumnBlock} from "./components/Column";
31 | import Row, {RowBlock} from "./components/Row";
32 | import Container, {ContainerBlock} from "./components/Container";
33 | import Text, {TextBlock} from "./components/Text";
34 | import Default from "./components/Default";
35 |
36 |
37 | export default (editor, config = {}) => {
38 | const c = config;
39 | const domc = editor.DomComponents;
40 | const blocks = c.blocks;
41 | const bm = editor.BlockManager;
42 | const cats = c.blockCategories;
43 |
44 | const traits = {
45 | id: {
46 | name: 'id',
47 | label: c.labels.trait_id,
48 | },
49 | for: {
50 | name: 'for',
51 | label: c.labels.trait_for,
52 | },
53 | name: {
54 | name: 'name',
55 | label: c.labels.trait_name,
56 | },
57 | placeholder: {
58 | name: 'placeholder',
59 | label: c.labels.trait_placeholder,
60 | },
61 | value: {
62 | name: 'value',
63 | label: c.labels.trait_value,
64 | },
65 | required: {
66 | type: 'checkbox',
67 | name: 'required',
68 | label: c.labels.trait_required,
69 | },
70 | checked: {
71 | label: c.labels.trait_checked,
72 | type: 'checkbox',
73 | name: 'checked',
74 | changeProp: 1
75 | }
76 | };
77 |
78 | if (cats.media) {
79 | if (blocks.image) {
80 | ImageBlock(bm, c.labels.image);
81 | Image(domc);
82 | }
83 |
84 | if (blocks.video) {
85 | Embed(domc);
86 | VideoBlock(bm, c.labels.video);
87 | Video(domc);
88 | }
89 | }
90 |
91 | // Rebuild the default component and add utility settings to it (border, bg, color, etc)
92 | if (cats.basic) {
93 | if (blocks.default) {
94 | Default(domc);
95 | }
96 |
97 | // Rebuild the text component and add display utility setting
98 | if (blocks.text) {
99 | TextBlock(bm, c.labels.text);
100 | Text(domc);
101 | }
102 |
103 | // Rebuild the link component with settings for collapse-control
104 | if (blocks.link) {
105 | LinkBlock(bm, c.labels.link);
106 | Link(editor);
107 | }
108 |
109 | // Basic
110 | /*if (blocks.list) {
111 | ListBlock(bm, c.labels.list)
112 | List(domc);
113 | }*/
114 |
115 | /*if (blocks.description_list) {
116 | }*/
117 |
118 | }
119 |
120 | // LAYOUT
121 | if (cats.layout) {
122 | if (blocks.container) {
123 | ContainerBlock(bm, c.labels.container);
124 | Container(domc);
125 | }
126 | if (blocks.row) {
127 | RowBlock(bm, c.labels.row);
128 | Row(domc);
129 | }
130 | if (blocks.column) {
131 | ColumnBlock(bm, c.labels.column);
132 | Column(domc, editor);
133 |
134 | ColumnBreakBlock(bm, c.labels.column_break);
135 | ColumnBreak(domc);
136 | }
137 | // Media object
138 | if (blocks.media_object) {
139 | MediaObjectBlock(bm, c.labels.media_object);
140 | MediaObject(domc);
141 | }
142 | }
143 |
144 | // Bootstrap COMPONENTS
145 | if (cats.components) {
146 | // Alert
147 | if (blocks.alert) {
148 | AlertBlock(bm, c.labels.alert);
149 | Alert(domc);
150 | }
151 |
152 | if (blocks.tabs) {
153 | TabsBlock(bm, c);
154 | TabsNavigation(domc, config);
155 | Tab(domc, config);
156 | TabsPanes(domc, config);
157 | TabPane(domc, config);
158 | }
159 |
160 | // Badge
161 | if (blocks.badge) {
162 | BadgeBlock(bm, c.labels.badge);
163 | Badge(domc);
164 | }
165 |
166 | // Card
167 | if (blocks.card) {
168 | CardBlock(bm, c);
169 | Card(domc, editor);
170 | }
171 |
172 | // Collapse
173 | if (blocks.collapse) {
174 | CollapseBlock(bm, c.labels.collapse);
175 | Collapse(editor);
176 | }
177 |
178 | // Dropdown
179 | if (blocks.dropdown) {
180 | DropDownBlock(bm, c.labels.dropdown);
181 | Dropdown(editor);
182 | }
183 |
184 | }
185 |
186 | // TYPOGRAPHY
187 | if (cats.typography) {
188 | if (blocks.header) {
189 | HeaderBlock(bm, c.labels.header);
190 | Header(domc);
191 | }
192 | if (blocks.paragraph) {
193 | ParagraphBlock(bm, c.labels.paragraph);
194 | Paragraph(domc);
195 | }
196 | }
197 |
198 | if(cats.forms) {
199 | if (blocks.form) {
200 | FormBlock(bm, c.labels.form);
201 | Form(domc, traits, config);
202 | }
203 |
204 | if (blocks.input) {
205 | InputBlock(bm, c.labels.input);
206 | Input(domc, traits, config);
207 |
208 | FileInputBlock(bm, c.labels.file_input);
209 | FileInput(domc, traits, config);
210 | }
211 |
212 | if (blocks.form_group_input) {
213 | InputGroupBlock(bm, c.labels.form_group_input);
214 | InputGroup(domc, traits, config);
215 | }
216 |
217 | if (blocks.textarea) {
218 | TextareaBlock(bm, c.labels.textarea);
219 | Textarea(domc, traits, config);
220 | }
221 |
222 | if (blocks.select) {
223 | SelectBlock(bm, c.labels.select);
224 | Select(editor, domc, traits, config);
225 | }
226 |
227 | if (blocks.checkbox) {
228 | CheckboxBlock(bm, c.labels.checkbox);
229 | Checkbox(domc, traits, config);
230 | }
231 |
232 | if (blocks.radio) {
233 | RadioBlock(bm, c.labels.radio);
234 | Radio(domc, traits, config);
235 | }
236 |
237 | if (blocks.label) {
238 | LabelBlock(bm, c.labels.label);
239 | Label(domc, traits, config);
240 | }
241 |
242 | if (blocks.button) {
243 | ButtonBlock(bm, c.labels.button);
244 | Button(domc);
245 | }
246 |
247 | if (blocks.button_group) {
248 | ButtonGroupBlock(bm, c.labels.button_group);
249 | ButtonGroup(domc);
250 | }
251 |
252 | if (blocks.button_toolbar) {
253 | ButtonToolbarBlock(bm, c.labels.button_toolbar, c);
254 | ButtonToolbar(domc);
255 | }
256 | }
257 |
258 | }
259 |
--------------------------------------------------------------------------------
/src/components/Alert.js:
--------------------------------------------------------------------------------
1 | import contexts from '../bootstrap-contexts';
2 | import exclamationIcon from "raw-loader!../icons/exclamation-triangle-solid.svg";
3 | import { capitalize } from "../utils";
4 |
5 | export const AlertBlock = (bm, label) => {
6 | bm.add('alert', {
7 | label: `
8 | ${exclamationIcon}
9 | ${label}
10 | `,
11 | category: 'Components',
12 | content: {
13 | type: 'alert',
14 | content: 'This is an alert—check it out!'
15 | }
16 | });
17 | };
18 |
19 | export default (domc) => {
20 | const textType = domc.getType('text');
21 | const textModel = textType.model;
22 | const textView = textType.view;
23 |
24 | domc.addType('alert', {
25 | extend: 'text',
26 | model: {
27 | defaults: Object.assign({}, textModel.prototype.defaults, {
28 | 'custom-name': 'Alert',
29 | tagName: 'div',
30 | classes: ['alert'],
31 | traits: [
32 | {
33 | type: 'class_select',
34 | options: [
35 | { value: '', name: 'None' },
36 | ...contexts.map(function (v) { return { value: 'alert-' + v, name: capitalize(v) } })
37 | ],
38 | label: 'Context'
39 | }
40 | ].concat(textModel.prototype.defaults.traits)
41 | })
42 | },
43 | isComponent(el) {
44 | if (el && el.classList && el.classList.contains('alert')) {
45 | return { type: 'alert' };
46 | }
47 | },
48 | view: textView
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Badge.js:
--------------------------------------------------------------------------------
1 | import contexts from '../bootstrap-contexts';
2 | import certificateIcon from "raw-loader!../icons/certificate-solid.svg";
3 | import { capitalize } from "../utils";
4 |
5 | export const BadgeBlock = (bm, label) => {
6 | bm.add('badge', {
7 | label: `
8 | ${certificateIcon}
9 | ${label}
10 | `,
11 | category: 'Components',
12 | content: {
13 | type: 'badge',
14 | content: 'New!'
15 | }
16 | });
17 | };
18 |
19 | export default (domc) => {
20 | const textType = domc.getType('text');
21 | const textModel = textType.model;
22 | const textView = textType.view;
23 |
24 | domc.addType('badge', {
25 | extend: 'text',
26 | model: {
27 | defaults: Object.assign({}, textModel.prototype.defaults, {
28 | 'custom-name': 'Badge',
29 | tagName: 'span',
30 | classes: ['badge'],
31 | traits: [
32 | {
33 | type: 'class_select',
34 | options: [
35 | { value: '', name: 'None' },
36 | ...contexts.map(function (v) { return { value: 'badge-' + v, name: capitalize(v) } })
37 | ],
38 | label: 'Context'
39 | },
40 | {
41 | type: 'class_select',
42 | options: [
43 | { value: '', name: 'Default' },
44 | { value: 'badge-pill', name: 'Pill' },
45 | ],
46 | label: 'Shape'
47 | }
48 | ].concat(textModel.prototype.defaults.traits)
49 | })
50 | },
51 | isComponent(el) {
52 | if (el && el.classList && el.classList.contains('badge')) {
53 | return { type: 'badge' };
54 | }
55 | },
56 | view: textView
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import contexts from '../bootstrap-contexts';
2 | import sizes from '../bootstrap-btn-sizes';
3 | import buttonIcon from "raw-loader!../icons/button.svg";
4 | import { capitalize } from "../utils";
5 |
6 | export const ButtonBlock = (bm, label) => {
7 | bm.add('button', {
8 | label: `${buttonIcon}${label}
`,
9 | category: 'Forms',
10 | content: 'Send ',
11 | });
12 | };
13 |
14 | export default (dc) => {
15 | const defaultType = dc.getType('default');
16 | const defaultModel = defaultType.model;
17 | const defaultView = defaultType.view;
18 |
19 | dc.addType('button', {
20 | model: {
21 | defaults: {
22 | ...defaultModel.prototype.defaults,
23 | 'custom-name': 'Button',
24 | droppable: false,
25 | attributes: {
26 | role: 'button'
27 | },
28 | classes: ['btn'],
29 | traits: [
30 | {
31 | type: 'content',
32 | label: 'Text',
33 | },
34 | {
35 | label: 'Type',
36 | type: 'select',
37 | name: 'type',
38 | options: [
39 | { value: 'submit', name: 'Submit' },
40 | { value: 'reset', name: 'Reset' },
41 | { value: 'button', name: 'Button' },
42 | ]
43 | },
44 | {
45 | type: 'class_select',
46 | options: [
47 | { value: '', name: 'None' },
48 | ...contexts.map((v) => { return { value: `btn-${v}`, name: capitalize(v) } }),
49 | ...contexts.map((v) => { return { value: `btn-outline-${v}`, name: capitalize(v) + ' (Outline)' } })
50 | ],
51 | label: 'Context'
52 | },
53 | {
54 | type: 'class_select',
55 | options: [
56 | { value: '', name: 'Default' },
57 | ...Object.keys(sizes).map((k) => { return { value: `btn-${k}`, name: sizes[k] } })
58 | ],
59 | label: 'Size'
60 | },
61 | {
62 | type: 'class_select',
63 | options: [
64 | { value: '', name: 'Inline' },
65 | { value: 'btn-block', name: 'Block' }
66 | ],
67 | label: 'Width'
68 | }
69 | ].concat(defaultModel.prototype.defaults.traits)
70 | },
71 | afterChange(e) {
72 | if (this.attributes.type === 'button') {
73 | if (this.attributes.classes.filter((klass) => { return klass.id === 'btn' }).length === 0) {
74 | this.changeType('link');
75 | }
76 | }
77 | }
78 | },
79 | isComponent(el) {
80 | if (el && el.classList && el.classList.contains('btn')) {
81 | return { type: 'button' };
82 | }
83 | },
84 | view: {
85 | events: {
86 | 'click': 'handleClick'
87 | },
88 |
89 | init() {
90 | this.listenTo(this.model, 'change:content', this.updateContent);
91 | },
92 |
93 | updateContent() {
94 | this.el.innerHTML = this.model.get('content')
95 | },
96 |
97 | handleClick(e) {
98 | e.preventDefault();
99 | },
100 | },
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/ButtonGroup.js:
--------------------------------------------------------------------------------
1 | import sizes from '../bootstrap-btn-sizes';
2 | import buttonIcon from "raw-loader!../icons/button.svg";
3 |
4 | export const ButtonGroupBlock = (bm, label) => {
5 | bm.add('button_group', {
6 | label: `
7 | ${buttonIcon}
8 | ${label}
9 | `,
10 | category: 'Forms',
11 | content: {
12 | type: 'button_group'
13 | }
14 | });
15 | };
16 |
17 | export default (dc) => {
18 |
19 | const defaultType = dc.getType('default');
20 | const defaultModel = defaultType.model;
21 | const defaultView = defaultType.view;
22 |
23 | dc.addType('button_group', {
24 | model: {
25 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
26 | 'custom-name': 'Button Group',
27 | tagName: 'div',
28 | classes: ['btn-group'],
29 | droppable: '.btn',
30 | attributes: {
31 | role: 'group'
32 | },
33 | traits: [
34 | {
35 | type: 'class_select',
36 | options: [
37 | { value: '', name: 'Default' },
38 | ...Object.keys(sizes).map(function (k) { return { value: 'btn-group-' + k, name: sizes[k] } })
39 | ],
40 | label: 'Size'
41 | },
42 | {
43 | type: 'class_select',
44 | options: [
45 | { value: '', name: 'Horizontal' },
46 | { value: 'btn-group-vertical', name: 'Vertical' },
47 | ],
48 | label: 'Size'
49 | },
50 | {
51 | type: 'Text',
52 | label: 'ARIA Label',
53 | name: 'aria-label',
54 | placeholder: 'A group of buttons'
55 | }
56 | ].concat(defaultModel.prototype.defaults.traits)
57 | })
58 | },
59 | isComponent(el) {
60 | if (el && el.classList && el.classList.contains('btn-group')) {
61 | return { type: 'button_group' };
62 | }
63 | },
64 | view: defaultView
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/src/components/ButtonToolbar.js:
--------------------------------------------------------------------------------
1 | import buttonIcon from "raw-loader!../icons/button.svg";
2 |
3 | export const ButtonToolbarBlock = (bm, label) => {
4 | bm.add('button_toolbar', {
5 | label: `
6 | ${buttonIcon}
7 | ${label}
8 | `,
9 | category: 'Forms',
10 | content: {
11 | type: 'button_toolbar'
12 | }
13 | });
14 | };
15 |
16 | export default (dc) => {
17 |
18 | const defaultType = dc.getType('default');
19 | const defaultModel = defaultType.model;
20 | const defaultView = defaultType.view;
21 |
22 | dc.addType('button_toolbar', {
23 | model: {
24 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
25 | 'custom-name': 'Button Toolbar',
26 | tagName: 'div',
27 | classes: ['btn-toolbar'],
28 | droppable: '.btn-group',
29 | attributes: {
30 | role: 'toolbar'
31 | },
32 | traits: [
33 | {
34 | type: 'Text',
35 | label: 'ARIA Label',
36 | name: 'aria-label',
37 | placeholder: 'A toolbar of button groups'
38 | }
39 | ].concat(defaultModel.prototype.defaults.traits)
40 | })
41 | },
42 | isComponent(el) {
43 | if (el && el.classList && el.classList.contains('btn-toolbar')) {
44 | return { type: 'button_toolbar' };
45 | }
46 | },
47 | view: defaultView
48 | });
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/Card.js:
--------------------------------------------------------------------------------
1 | import cardIcon from "raw-loader!../icons/credit-card-solid.svg";
2 |
3 | export const CardBlock = (bm, c) => {
4 | bm.add('card', {
5 | label: `
6 | ${cardIcon}
7 | ${c.labels.card}
8 | `,
9 | category: 'Components',
10 | content: {
11 | type: 'card'
12 | }
13 | });
14 | bm.add('card_container', {
15 | label: `
16 | ${cardIcon}
17 | ${c.labels.card_container}
18 | `,
19 | category: 'Components',
20 | content: {
21 | type: 'card_container'
22 | }
23 | });
24 | };
25 |
26 | export default (domc, editor) => {
27 | const comps = editor.DomComponents;
28 | const defaultType = comps.getType('default');
29 | const defaultModel = defaultType.model;
30 | const defaultView = defaultType.view;
31 | const imageType = domc.getType('image');
32 | const imageModel = imageType.model;
33 | const imageView = imageType.view;
34 |
35 | domc.addType('card', {
36 | model: {
37 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
38 | 'custom-name': 'Card',
39 | classes: ['card'],
40 | traits: [
41 | {
42 | type: 'checkbox',
43 | label: 'Image Top',
44 | name: 'card-img-top',
45 | changeProp: 1
46 | },
47 | {
48 | type: 'checkbox',
49 | label: 'Header',
50 | name: 'card-header',
51 | changeProp: 1
52 | },
53 | {
54 | type: 'checkbox',
55 | label: 'Image',
56 | name: 'card-img',
57 | changeProp: 1
58 | },
59 | {
60 | type: 'checkbox',
61 | label: 'Image Overlay',
62 | name: 'card-img-overlay',
63 | changeProp: 1
64 | },
65 | {
66 | type: 'checkbox',
67 | label: 'Body',
68 | name: 'card-body',
69 | changeProp: 1
70 | },
71 | {
72 | type: 'checkbox',
73 | label: 'Footer',
74 | name: 'card-footer',
75 | changeProp: 1
76 | },
77 | {
78 | type: 'checkbox',
79 | label: 'Image Bottom',
80 | name: 'card-img-bottom',
81 | changeProp: 1
82 | }
83 | ].concat(defaultModel.prototype.defaults.traits)
84 | }),
85 | init2() {
86 | this.listenTo(this, 'change:card-img-top', this.cardImageTop);
87 | this.listenTo(this, 'change:card-header', this.cardHeader);
88 | this.listenTo(this, 'change:card-img', this.cardImage);
89 | this.listenTo(this, 'change:card-img-overlay', this.cardImageOverlay);
90 | this.listenTo(this, 'change:card-body', this.cardBody);
91 | this.listenTo(this, 'change:card-footer', this.cardFooter);
92 | this.listenTo(this, 'change:card-img-bottom', this.cardImageBottom);
93 | this.components().comparator = 'card-order';
94 | this.set('card-img-top', true);
95 | this.set('card-body', true);
96 | },
97 | cardImageTop() { this.createCardComponent('card-img-top'); },
98 | cardHeader() { this.createCardComponent('card-header'); },
99 | cardImage() { this.createCardComponent('card-img'); },
100 | cardImageOverlay() { this.createCardComponent('card-img-overlay'); },
101 | cardBody() { this.createCardComponent('card-body'); },
102 | cardFooter() { this.createCardComponent('card-footer'); },
103 | cardImageBottom() { this.createCardComponent('card-img-bottom'); },
104 | createCardComponent(prop) {
105 | const state = this.get(prop);
106 | const type = prop.replace(/-/g, '_').replace(/img/g, 'image')
107 | let children = this.components();
108 | let existing = children.filter(function (comp) {
109 | return comp.attributes.type === type;
110 | })[0]; // should only be one of each.
111 |
112 | if (state && !existing) {
113 | var comp = children.add({
114 | type: type
115 | });
116 | let comp_children = comp.components();
117 | if (prop === 'card-header') {
118 | comp_children.add({
119 | type: 'header',
120 | tagName: 'h4',
121 | style: { 'margin-bottom': '0px' },
122 | content: 'Card Header'
123 | });
124 | }
125 | if (prop === 'card-img-overlay') {
126 | comp_children.add({
127 | type: 'header',
128 | tagName: 'h4',
129 | classes: ['card-title'],
130 | content: 'Card title'
131 | });
132 | comp_children.add({
133 | type: 'text',
134 | tagName: 'p',
135 | classes: ['card-text'],
136 | content: "Some quick example text to build on the card title and make up the bulk of the card's content."
137 | });
138 | }
139 | if (prop === 'card-body') {
140 | comp_children.add({
141 | type: 'header',
142 | tagName: 'h4',
143 | classes: ['card-title'],
144 | content: 'Card title'
145 | });
146 | comp_children.add({
147 | type: 'header',
148 | tagName: 'h6',
149 | classes: ['card-subtitle', 'text-muted', 'mb-2'],
150 | content: 'Card subtitle'
151 | });
152 | comp_children.add({
153 | type: 'text',
154 | tagName: 'p',
155 | classes: ['card-text'],
156 | content: "Some quick example text to build on the card title and make up the bulk of the card's content."
157 | });
158 | comp_children.add({
159 | type: 'link',
160 | classes: ['card-link'],
161 | href: '#',
162 | content: 'Card link'
163 | });
164 | comp_children.add({
165 | type: 'link',
166 | classes: ['card-link'],
167 | href: '#',
168 | content: 'Another link'
169 | });
170 | }
171 | this.order();
172 | } else if (!state) {
173 | existing.destroy();
174 | }
175 | },
176 | order() {
177 |
178 | }
179 | },
180 | isComponent(el) {
181 | if (el && el.classList && el.classList.contains('card')) {
182 | return { type: 'card' };
183 | }
184 | },
185 | view: defaultView
186 | });
187 |
188 | domc.addType('card_image_top', {
189 | extend: 'image',
190 | model: {
191 | defaults: Object.assign({}, imageModel.prototype.defaults, {
192 | 'custom-name': 'Card Image Top',
193 | classes: ['card-img-top'],
194 | 'card-order': 1
195 | })
196 | },
197 | isComponent(el) {
198 | if (el && el.classList && el.classList.contains('card-img-top')) {
199 | return { type: 'card_image_top' };
200 | }
201 | },
202 | view: imageView
203 | });
204 |
205 | domc.addType('card_header', {
206 | model: {
207 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
208 | 'custom-name': 'Card Header',
209 | classes: ['card-header'],
210 | 'card-order': 2
211 | })
212 | },
213 | isComponent(el) {
214 | if (el && el.classList && el.classList.contains('card-header')) {
215 | return { type: 'card_header' };
216 | }
217 | },
218 | view: defaultView
219 | });
220 |
221 | domc.addType('card_image', {
222 | extend: 'image',
223 | model: {
224 | defaults: Object.assign({}, imageModel.prototype.defaults, {
225 | 'custom-name': 'Card Image',
226 | classes: ['card-img'],
227 | 'card-order': 3
228 | })
229 | },
230 | isComponent(el) {
231 | if (el && el.classList && el.classList.contains('card-img')) {
232 | return { type: 'card_image' };
233 | }
234 | },
235 | view: imageView
236 | });
237 |
238 | domc.addType('card_image_overlay', {
239 | model: {
240 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
241 | 'custom-name': 'Card Image Overlay',
242 | classes: ['card-img-overlay'],
243 | 'card-order': 4
244 | })
245 | },
246 | isComponent(el) {
247 | if (el && el.classList && el.classList.contains('card-img-overlay')) {
248 | return { type: 'card_image_overlay' };
249 | }
250 | },
251 | view: defaultView
252 | });
253 |
254 | domc.addType('card_body', {
255 | model: {
256 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
257 | 'custom-name': 'Card Body',
258 | classes: ['card-body'],
259 | 'card-order': 5
260 | })
261 | },
262 | isComponent(el) {
263 | if (el && el.classList && el.classList.contains('card-body')) {
264 | return { type: 'card_body' };
265 | }
266 | },
267 | view: defaultView
268 | });
269 |
270 | domc.addType('card_footer', {
271 | model: {
272 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
273 | 'custom-name': 'Card Footer',
274 | classes: ['card-footer'],
275 | 'card-order': 6
276 | })
277 | },
278 | isComponent(el) {
279 | if (el && el.classList && el.classList.contains('card-footer')) {
280 | return { type: 'card_footer' };
281 | }
282 | },
283 | view: defaultView
284 | });
285 |
286 | domc.addType('card_image_bottom', {
287 | extend: 'image',
288 | model: {
289 | defaults: Object.assign({}, imageModel.prototype.defaults, {
290 | 'custom-name': 'Card Image Bottom',
291 | classes: ['card-img-bottom'],
292 | 'card-order': 7
293 | })
294 | },
295 | isComponent(el) {
296 | if (el && el.classList && el.classList.contains('card-img-bottom')) {
297 | return { type: 'card_image_bottom' };
298 | }
299 | },
300 | view: imageView
301 | });
302 |
303 | domc.addType('card_container', {
304 | model: {
305 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
306 | 'custom-name': 'Card Container',
307 | classes: ['card-group'],
308 | droppable: '.card',
309 | traits: [
310 | {
311 | type: 'class_select',
312 | options: [
313 | { value: 'card-group', name: 'Group' },
314 | { value: 'card-deck', name: 'Deck' },
315 | { value: 'card-columns', name: 'Columns' },
316 | ],
317 | label: 'Layout',
318 | }
319 | ].concat(defaultModel.prototype.defaults.traits)
320 | })
321 | },
322 | isComponent(el) {
323 | const css = Array.from(el.classList || []);
324 | const includes = ['card-group', 'card-deck', 'card-columns'];
325 | const intersection = css.filter(x => includes.includes(x));
326 |
327 | if (el && el.classList && intersection.length) {
328 | return { type: 'card_container' };
329 | }
330 | },
331 | view: defaultView
332 | });
333 |
334 | }
335 |
--------------------------------------------------------------------------------
/src/components/Checkbox.js:
--------------------------------------------------------------------------------
1 | import checkIcon from "raw-loader!../icons/check-square-solid.svg";
2 |
3 | export const CheckboxBlock = (bm, label) => {
4 | bm.add('checkbox', {
5 | label: `
6 | ${checkIcon}
7 | ${label}
8 | `,
9 | category: 'Forms',
10 | content: `
11 |
12 |
13 |
14 | Default checkbox
15 |
16 |
17 | `,
18 | });
19 | };
20 |
21 | export default (dc, traits, config = {}) => {
22 | const defaultType = dc.getType('default');
23 | const defaultModel = defaultType.model;
24 | const defaultView = defaultType.view;
25 | const inputType = dc.getType('input');
26 | const inputModel = inputType.model;
27 |
28 | dc.addType('checkbox', {
29 | model: {
30 | defaults: {
31 | ...inputModel.prototype.defaults,
32 | 'custom-name': config.labels.checkbox_name,
33 | copyable: false,
34 | droppable: false,
35 | attributes: { type: 'checkbox' },
36 | traits: [
37 | traits.id,
38 | traits.name,
39 | traits.value,
40 | traits.required,
41 | traits.checked
42 | ],
43 | },
44 |
45 | init() {
46 | this.listenTo(this, 'change:checked', this.handleChecked);
47 | },
48 |
49 | handleChecked() {
50 | let checked = this.get('checked');
51 | let attrs = this.get('attributes');
52 | const view = this.view;
53 |
54 | if (checked) {
55 | attrs.checked = true;
56 | } else {
57 | delete attrs.checked;
58 | }
59 |
60 | if (view) {
61 | view.el.checked = checked
62 | }
63 |
64 | this.set('attributes', { ...attrs });
65 | }
66 | },
67 | isComponent(el) {
68 | if (el.tagName === 'INPUT' && el.type === 'checkbox') {
69 | return { type: 'checkbox' };
70 | }
71 | },
72 | view: {
73 | events: {
74 | 'click': 'handleClick',
75 | },
76 |
77 | handleClick(e) {
78 | e.preventDefault();
79 | },
80 | },
81 | });
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/Collapse.js:
--------------------------------------------------------------------------------
1 | import compressIcon from "raw-loader!../icons/compress-solid.svg";
2 |
3 | export const CollapseBlock = (bm, label) => {
4 | bm.add('collapse', {
5 | label: `
6 | ${compressIcon}
7 | ${label}
8 | `,
9 | category: 'Components',
10 | content: {
11 | type: 'collapse'
12 | }
13 | });
14 | };
15 |
16 | export default (editor) => {
17 | const comps = editor.DomComponents;
18 | const defaultType = comps.getType('default');
19 | const defaultModel = defaultType.model;
20 | const defaultView = defaultType.view;
21 |
22 | comps.addType('collapse', {
23 | model: {
24 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
25 | 'custom-name': 'Dropdown',
26 | classes: ['collapse'],
27 | droppable: true,
28 | traits: [
29 | {
30 | type: 'class_select',
31 | options: [
32 | { value: '', name: 'Closed' },
33 | { value: 'show', name: 'Open' }
34 | ],
35 | label: 'Initial state'
36 | }
37 | ].concat(defaultModel.prototype.defaults.traits)
38 | }),
39 | /*init2() {
40 | window.asdf = this;
41 | const toggle = {
42 | type: 'button',
43 | content: 'Click to toggle',
44 | classes: ['btn', 'dropdown-toggle']
45 | }
46 | const toggle_comp = this.append(toggle)[0];
47 | const menu = {
48 | type: 'dropdown_menu'
49 | }
50 | const menu_comp = this.append(menu)[0];
51 | this.setupToggle(null, null, {force: true});
52 | const comps = this.components();
53 | comps.bind('add', this.setupToggle.bind(this));
54 | comps.bind('change', this.setupToggle.bind(this));
55 | comps.bind('remove', this.setupToggle.bind(this));
56 | const classes = this.get('classes');
57 | classes.bind('add', this.setupToggle.bind(this));
58 | classes.bind('change', this.setupToggle.bind(this));
59 | classes.bind('remove', this.setupToggle.bind(this));
60 | },
61 | setupToggle(a, b, options = {}) {
62 | const toggle = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-toggle'))[0];
63 | // raise error if toggle not found
64 | const menu = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-menu'))[0];
65 | // raise error if menu not found
66 |
67 | if(options.force !== true && options.ignore === true) {
68 | return;
69 | }
70 |
71 | if(toggle && menu) {
72 |
73 | function hasEvent(comp) {
74 | let eca = comp._events['change:attributes'];
75 | if(!eca) return false;
76 | return eca.filter(e => e.callback.name == 'setupToggle').length != 0;
77 | }
78 |
79 | // setup event listeners if they aren't set
80 | if(!hasEvent(toggle)) {
81 | this.listenTo(toggle, 'change:attributes', this.setupToggle);
82 | }
83 | if(!hasEvent(menu)) {
84 | this.listenTo(menu, 'change:attributes', this.setupToggle);
85 | }
86 |
87 | // setup toggle
88 | var toggle_attrs = toggle.getAttributes();
89 | toggle_attrs['role'] = 'button'; // if A
90 | var menu_attrs = menu.getAttributes();
91 | if(!toggle_attrs.hasOwnProperty('data-toggle')) {
92 | toggle_attrs['data-toggle'] = 'dropdown';
93 | }
94 | if(!toggle_attrs.hasOwnProperty('aria-haspopup')) {
95 | toggle_attrs['aria-haspopup'] = true;
96 | }
97 | const dropdown_classes = this.getAttributes().class.split(' ');
98 | toggle_attrs['aria-expanded'] = dropdown_classes.includes('show');
99 | toggle.set('attributes', toggle_attrs, {ignore: true});
100 | // setup menu
101 | // toggle needs ID for aria-labelled on the menu, could alert here
102 | if(toggle_attrs.hasOwnProperty('id')) {
103 | menu_attrs['aria-labelledby'] = toggle_attrs.id;
104 | } else {
105 | delete menu_attrs['aria-labelledby'];
106 | }
107 | menu.set('attributes', menu_attrs, {ignore: true});
108 | }
109 | }*/
110 | },
111 | isComponent(el) {
112 | if (el && el.classList && el.classList.contains('dropdown')) {
113 | return { type: 'dropdown' };
114 | }
115 | },
116 | view: {
117 | /*init() {
118 | this.model.setupToggle
119 | }*/
120 | }
121 | });
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/Column.js:
--------------------------------------------------------------------------------
1 | import columnsIcon from "raw-loader!../icons/columns-solid.svg";
2 |
3 | export const ColumnBlock = (bm, label) => {
4 | bm.add('column').set({
5 | label: `
6 | ${columnsIcon}
7 | ${label}
8 | `,
9 | category: 'Layout',
10 | content: {
11 | type: 'column',
12 | classes: ['col']
13 | }
14 | });
15 | };
16 |
17 | export default (domc, editor) => {
18 | const defaultType = domc.getType('default');
19 | const defaultModel = defaultType.model;
20 | const defaultView = defaultType.view;
21 | const spans = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
22 |
23 | domc.addType('column', {
24 | model: {
25 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
26 | 'custom-name': 'Column',
27 | draggable: '.row',
28 | droppable: true,
29 | resizable: {
30 | updateTarget: (el, rect, opt) => {
31 | const selected = editor.getSelected();
32 | if (!selected) { return; }
33 |
34 | //compute the current screen size (bootstrap semantic)
35 | const docWidth = el.getRootNode().body.offsetWidth;
36 | let currentSize = "";
37 | if (docWidth >= 1200) {
38 | currentSize = "xl";
39 | } else if (docWidth >= 992) {
40 | currentSize = "lg";
41 | } else if (docWidth >= 768) {
42 | currentSize = "md";
43 | } else if (docWidth >= 576) {
44 | currentSize = "sm";
45 | }
46 |
47 | //compute the threshold when add on remove 1 col span to the element
48 | const row = el.parentElement;
49 | const oneColWidth = row.offsetWidth / 12;
50 | //the threshold is half one column width
51 | const threshold = oneColWidth * 0.5;
52 |
53 | //check if we are growing or shrinking the column
54 | const grow = rect.w > el.offsetWidth + threshold;
55 | const shrink = rect.w < el.offsetWidth - threshold;
56 | if (grow || shrink) {
57 | let testRegexp = new RegExp("^col-" + currentSize + "-\\d{1,2}$");
58 | if (!currentSize) {
59 | testRegexp = new RegExp("^col-\\d{1,2}$");
60 | }
61 | let found = false;
62 | let sizesSpans = {};
63 | let oldSpan = 0;
64 | let oldClass = null;
65 | for (let cl of el.classList) {
66 | if (cl.indexOf("col-") === 0) {
67 | let [c, size, span] = cl.split("-");
68 | if (!span) {
69 | span = size;
70 | size = "";
71 | }
72 | sizesSpans[size] = span;
73 | if (size === currentSize) {
74 | //found the col-XX-99 class
75 | oldClass = cl;
76 | oldSpan = span;
77 | found = true;
78 | }
79 | }
80 | }
81 |
82 | if (!found) {
83 | const sizeOrder = ["", "xs", "sm", "md", "lg", "xl"];
84 | for (let s of sizeOrder) {
85 | if (sizesSpans[s]) {
86 | oldSpan = sizesSpans[s];
87 | found = true;
88 | }
89 | if (s === currentSize) {
90 | break;
91 | }
92 | }
93 | }
94 |
95 | let newSpan = Number(oldSpan);
96 | if (grow) {
97 | newSpan++;
98 | } else {
99 | newSpan--;
100 | }
101 | if (newSpan > 12) { newSpan = 12; }
102 | if (newSpan < 1) { newSpan = 1; }
103 |
104 | let newClass = "col-" + currentSize + "-" + newSpan;
105 | if (!currentSize) {
106 | newClass = "col-" + newSpan;
107 | }
108 | //update the class
109 | selected.addClass(newClass);
110 | if (oldClass && oldClass !== newClass) {
111 | selected.removeClass(oldClass);
112 | }
113 | //notify the corresponding trait to update its value accordingly
114 | selected.getTrait((currentSize || "xs") + "_width").view.postUpdate();
115 | }
116 | },
117 | tl: 0,
118 | tc: 0,
119 | tr: 0,
120 | cl: 0,
121 | cr: 1,
122 | bl: 0,
123 | bc: 0,
124 | br: 0
125 | },
126 | traits: [
127 | {
128 | id: "xs_width",
129 | type: 'class_select',
130 | options: [
131 | { value: 'col', name: 'Equal' },
132 | { value: 'col-auto', name: 'Variable' },
133 | ...spans.map(function (i) { return { value: 'col-' + i, name: i + '/12' } })
134 | ],
135 | label: 'XS Width',
136 | },
137 | {
138 | id: "sm_width",
139 | type: 'class_select',
140 | options: [
141 | { value: '', name: 'None' },
142 | { value: 'col-sm', name: 'Equal' },
143 | { value: 'col-sm-auto', name: 'Variable' },
144 | ...spans.map(function (i) { return { value: 'col-sm-' + i, name: i + '/12' } })
145 | ],
146 | label: 'SM Width',
147 | },
148 | {
149 | id: "md_width",
150 | type: 'class_select',
151 | options: [
152 | { value: '', name: 'None' },
153 | { value: 'col-md', name: 'Equal' },
154 | { value: 'col-md-auto', name: 'Variable' },
155 | ...spans.map(function (i) { return { value: 'col-md-' + i, name: i + '/12' } })
156 | ],
157 | label: 'MD Width',
158 | },
159 | {
160 | id: "lg_width",
161 | type: 'class_select',
162 | options: [
163 | { value: '', name: 'None' },
164 | { value: 'col-lg', name: 'Equal' },
165 | { value: 'col-lg-auto', name: 'Variable' },
166 | ...spans.map(function (i) { return { value: 'col-lg-' + i, name: i + '/12' } })
167 | ],
168 | label: 'LG Width',
169 | },
170 | {
171 | id: "xl_width",
172 | type: 'class_select',
173 | options: [
174 | { value: '', name: 'None' },
175 | { value: 'col-xl', name: 'Equal' },
176 | { value: 'col-xl-auto', name: 'Variable' },
177 | ...spans.map(function (i) { return { value: 'col-xl-' + i, name: i + '/12' } })
178 | ],
179 | label: 'XL Width',
180 | },
181 | {
182 | type: 'class_select',
183 | options: [
184 | { value: '', name: 'None' },
185 | ...spans.map(function (i) { return { value: 'offset-' + i, name: i + '/12' } })
186 | ],
187 | label: 'XS Offset',
188 | },
189 | {
190 | type: 'class_select',
191 | options: [
192 | { value: '', name: 'None' },
193 | ...spans.map(function (i) { return { value: 'offset-sm-' + i, name: i + '/12' } })
194 | ],
195 | label: 'SM Offset',
196 | },
197 | {
198 | type: 'class_select',
199 | options: [
200 | { value: '', name: 'None' },
201 | ...spans.map(function (i) { return { value: 'offset-md-' + i, name: i + '/12' } })
202 | ],
203 | label: 'MD Offset',
204 | },
205 | {
206 | type: 'class_select',
207 | options: [
208 | { value: '', name: 'None' },
209 | ...spans.map(function (i) { return { value: 'offset-lg-' + i, name: i + '/12' } })
210 | ],
211 | label: 'LG Offset',
212 | },
213 | {
214 | type: 'class_select',
215 | options: [
216 | { value: '', name: 'None' },
217 | ...spans.map(function (i) { return { value: 'offset-xl-' + i, name: i + '/12' } })
218 | ],
219 | label: 'XL Offset',
220 | },
221 | ].concat(defaultModel.prototype.defaults.traits)
222 | }),
223 | },
224 | isComponent(el) {
225 | let match = false;
226 | if (el && el.classList) {
227 | el.classList.forEach(function (klass) {
228 | if (klass == "col" || klass.match(/^col-/)) {
229 | match = true;
230 | }
231 | });
232 | }
233 | if (match) return { type: 'column' };
234 | },
235 | view: defaultView
236 | });
237 | }
238 |
--------------------------------------------------------------------------------
/src/components/ColumnBreak.js:
--------------------------------------------------------------------------------
1 | import equalsIcon from "raw-loader!../icons/equals-solid.svg";
2 |
3 | export const ColumnBreakBlock = (bm, label) => {
4 | bm.add('column_break').set({
5 | label: `
6 | ${equalsIcon}
7 | ${label}
8 | `,
9 | category: 'Layout',
10 | content: {
11 | type: 'column_break'
12 | }
13 | });
14 | };
15 |
16 | export default (domc) => {
17 | const defaultType = domc.getType('default');
18 | const defaultModel = defaultType.model;
19 | const defaultView = defaultType.view;
20 |
21 | domc.addType('column_break', {
22 | model: {
23 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
24 | 'custom-name': 'Column Break',
25 | tagName: 'div',
26 | classes: ['w-100']
27 | })
28 | },
29 | isComponent(el) {
30 | if (el && el.classList && el.classList.contains('w-100')) { // also check if parent is `.row`
31 | return { type: 'column_break' };
32 | }
33 | },
34 | view: defaultView
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Container.js:
--------------------------------------------------------------------------------
1 | import windowIcon from "raw-loader!../icons/window-maximize-solid.svg";
2 |
3 | export const ContainerBlock = (bm, label) => {
4 | bm.add('container').set({
5 | label: `
6 | ${windowIcon}
7 | ${label}
8 | `,
9 | category: 'Layout',
10 | content: {
11 | type: 'container',
12 | classes: ['container']
13 | }
14 | });
15 | };
16 |
17 | export default (domc) => {
18 | const defaultType = domc.getType('default');
19 | const defaultModel = defaultType.model;
20 | const defaultView = defaultType.view;
21 |
22 | domc.addType('container', {
23 | model: {
24 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
25 | 'custom-name': 'Container',
26 | tagName: 'div',
27 | droppable: true,
28 | traits: [
29 | {
30 | type: 'class_select',
31 | options: [
32 | { value: 'container', name: 'Fixed' },
33 | { value: 'container-fluid', name: 'Fluid' }
34 | ],
35 | label: 'Width'
36 | }
37 | ].concat(defaultModel.prototype.defaults.traits)
38 | })
39 | },
40 | isComponent(el) {
41 | if (el && el.classList && (el.classList.contains('container') || el.classList.contains('container-fluid'))) {
42 | return { type: 'container' };
43 | }
44 | },
45 | view: defaultView
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Default.js:
--------------------------------------------------------------------------------
1 | import contexts from '../bootstrap-contexts';
2 | import { capitalize } from "../utils";
3 |
4 | export default (domc) => {
5 | const contexts_w_white = contexts.concat(['white']);
6 | const defaultType = domc.getType('default');
7 | const defaultModel = defaultType.model;
8 | const defaultView = defaultType.view;
9 |
10 | domc.addType('default', {
11 | model: {
12 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
13 | tagName: 'div',
14 | traits: [
15 | {
16 | type: 'class_select',
17 | options: [
18 | { value: '', name: 'Default' },
19 | ...contexts_w_white.map(function (v) { return { value: 'text-' + v, name: capitalize(v) } })
20 | ],
21 | label: 'Text color'
22 | },
23 | {
24 | type: 'class_select',
25 | options: [
26 | { value: '', name: 'Default' },
27 | ...contexts_w_white.map(function (v) { return { value: 'bg-' + v, name: capitalize(v) } })
28 | ],
29 | label: 'Background color'
30 | },
31 | {
32 | type: 'class_select',
33 | options: [
34 | { value: '', name: 'Default' },
35 | { value: 'border', name: 'Full' },
36 | { value: 'border-top-0', name: 'No top' },
37 | { value: 'border-right-0', name: 'No right' },
38 | { value: 'border-bottom-0', name: 'No bottom' },
39 | { value: 'border-left-0', name: 'No left' },
40 | { value: 'border-0', name: 'None' }
41 | ],
42 | label: 'Border width'
43 | },
44 | {
45 | type: 'class_select',
46 | options: [
47 | { value: '', name: 'Default' },
48 | ...contexts_w_white.map(function (v) { return { value: 'border border-' + v, name: capitalize(v) } })
49 | ],
50 | label: 'Border color'
51 | },
52 | {
53 | type: 'class_select',
54 | options: [
55 | { value: '', name: 'Default' },
56 | { value: 'rounded', name: 'Rounded' },
57 | { value: 'rounded-top', name: 'Rounded top' },
58 | { value: 'rounded-right', name: 'Rounded right' },
59 | { value: 'rounded-bottom', name: 'Rounded bottom' },
60 | { value: 'rounded-left', name: 'Rounded left' },
61 | { value: 'rounded-circle', name: 'Circle' },
62 | { value: 'rounded-0', name: 'Square' },
63 | ],
64 | label: 'Border radius'
65 | },
66 | {
67 | type: 'text',
68 | label: 'ID',
69 | name: 'id',
70 | placeholder: 'my_element'
71 | },
72 | {
73 | type: 'text',
74 | label: 'Title',
75 | name: 'title',
76 | placeholder: 'My Element'
77 | }
78 | ] //.concat(defaultModel.prototype.defaults.traits)
79 | }),
80 | init() {
81 | const classes = this.get('classes');
82 | classes.bind('add', this.classesChanged.bind(this));
83 | classes.bind('change', this.classesChanged.bind(this));
84 | classes.bind('remove', this.classesChanged.bind(this));
85 | this.init2();
86 | },
87 | /* BS comps use init2, not init */
88 | init2() { },
89 | /* method where we can check if we should changeType */
90 | classesChanged() { },
91 | /* replace the comp with a copy of a different type */
92 | changeType(new_type) {
93 | const coll = this.collection;
94 | const at = coll.indexOf(this);
95 | const button_opts = {
96 | type: new_type,
97 | style: this.getStyle(),
98 | attributes: this.getAttributes(),
99 | content: this.view.el.innerHTML
100 | }
101 | coll.remove(this);
102 | coll.add(button_opts, { at });
103 | this.destroy();
104 | }
105 | },
106 | view: defaultView
107 | });
108 | }
109 |
--------------------------------------------------------------------------------
/src/components/Dropdown.js:
--------------------------------------------------------------------------------
1 | /*
2 | known issues:
3 | - BS dropdown JS isn't attached if you remove the existing toggle and add a new one
4 | */
5 |
6 | import caretIcon from "raw-loader!../icons/caret-square-down-regular.svg";
7 |
8 | export const DropDownBlock = (bm, label) => {
9 | bm.add('dropdown', {
10 | label: `
11 | ${caretIcon}
12 | ${label}
13 | `,
14 | category: 'Components',
15 | content: {
16 | type: 'dropdown'
17 | }
18 | });
19 | /*bm.add('dropdown_menu', {
20 | label: c.labels.dropdown_menu,
21 | category: 'Components',
22 | attributes: {class:'fa fa-caret-down'},
23 | content: {
24 | type: 'dropdown_menu'
25 | }
26 | });
27 | bm.add('dropdown_item', {
28 | label: c.labels.dropdown_item,
29 | category: 'Components',
30 | attributes: {class:'fa fa-link'},
31 | content: {
32 | type: 'dropdown_item'
33 | }
34 | });*/
35 | };
36 |
37 | export default (editor) => {
38 | const comps = editor.DomComponents;
39 | const defaultType = comps.getType('default');
40 | const defaultModel = defaultType.model;
41 | const defaultView = defaultType.view;
42 |
43 | function hasEvent(comp) {
44 | let eca = comp._events['change:attributes'];
45 | if (!eca) return false;
46 | return eca.filter(e => e.callback.name === 'setupToggle').length !== 0;
47 | }
48 |
49 | comps.addType('dropdown', {
50 | model: {
51 | defaults: {
52 | ...defaultModel.prototype.defaults,
53 | 'custom-name': 'Dropdown',
54 | classes: ['dropdown'],
55 | droppable: 'a, button, .dropdown-menu',
56 | traits: [
57 | {
58 | type: 'select',
59 | label: 'Initial state',
60 | name: 'initial_state',
61 | options: [
62 | { value: '', name: 'Closed' },
63 | { value: 'show', name: 'Open' }
64 | ],
65 | }
66 | ].concat(defaultModel.prototype.defaults.traits),
67 | },
68 |
69 | init2() {
70 | const toggle = {
71 | type: 'button',
72 | content: 'Click to toggle',
73 | classes: ['btn', 'dropdown-toggle']
74 | };
75 | const toggle_comp = this.append(toggle)[0];
76 | const menu = {
77 | type: 'dropdown_menu'
78 | };
79 | const menu_comp = this.append(menu)[0];
80 | this.setupToggle(null, null, { force: true });
81 | const comps = this.components();
82 | comps.bind('add', this.setupToggle.bind(this));
83 | comps.bind('remove', this.setupToggle.bind(this));
84 | const classes = this.get('classes');
85 | classes.bind('add', this.setupToggle.bind(this));
86 | classes.bind('change', this.setupToggle.bind(this));
87 | classes.bind('remove', this.setupToggle.bind(this));
88 | },
89 |
90 | setupToggle(a, b, options = {}) {
91 | const toggle = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-toggle'))[0];
92 | const menu = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-menu'))[0];
93 |
94 | if (options.force !== true && options.ignore === true) {
95 | return;
96 | }
97 |
98 | if (toggle && menu) {
99 |
100 | // setup event listeners if they aren't set
101 | if (!hasEvent(toggle)) {
102 | this.listenTo(toggle, 'change:attributes', this.setupToggle);
103 | }
104 | if (!hasEvent(menu)) {
105 | this.listenTo(menu, 'change:attributes', this.setupToggle);
106 | }
107 |
108 | // setup toggle
109 | const toggle_attrs = toggle.getAttributes();
110 | toggle_attrs['role'] = 'button';
111 | const menu_attrs = menu.getAttributes();
112 | if (!toggle_attrs.hasOwnProperty('data-toggle')) {
113 | toggle_attrs['data-toggle'] = 'dropdown';
114 | }
115 | if (!toggle_attrs.hasOwnProperty('aria-haspopup')) {
116 | toggle_attrs['aria-haspopup'] = true;
117 | }
118 |
119 | toggle.set('attributes', toggle_attrs, { ignore: true });
120 |
121 | // setup menu
122 | // toggle needs ID for aria-labelled on the menu, could alert here
123 | if (toggle_attrs.hasOwnProperty('id')) {
124 | menu_attrs['aria-labelledby'] = toggle_attrs.id;
125 | } else {
126 | delete menu_attrs['aria-labelledby'];
127 | }
128 | menu.set('attributes', menu_attrs, { ignore: true });
129 | }
130 | },
131 |
132 | updated(property, value) {
133 | if (value.hasOwnProperty('initial_state')) {
134 | const menu = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-menu'))[0];
135 | const attrs = menu.getAttributes();
136 | const classes = attrs.class.split(' ');
137 |
138 | if (classes.includes('show')) {
139 | // Close the menu
140 | attrs['aria-expanded'] = false;
141 | menu.removeClass('show');
142 | } else {
143 | // Open the menu
144 | attrs['aria-expanded'] = true;
145 | menu.addClass('show');
146 | }
147 | }
148 | },
149 |
150 | },
151 | isComponent(el) {
152 | if (el && el.classList && el.classList.contains('dropdown')) {
153 | return { type: 'dropdown' };
154 | }
155 | },
156 | view: defaultView
157 | });
158 |
159 | // need aria-labelledby to equal dropdown-toggle id
160 | // need to insert dropdown-item class on links when added
161 | comps.addType('dropdown_menu', {
162 | model: {
163 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
164 | 'custom-name': 'Dropdown Menu',
165 | classes: ['dropdown-menu'],
166 | draggable: '.dropdown',
167 | droppable: true
168 | }),
169 | init2() {
170 | const header = {
171 | type: 'header',
172 | tagName: 'h6',
173 | classes: ['dropdown-header'],
174 | content: 'Dropdown header'
175 | };
176 | const link = {
177 | type: 'link',
178 | classes: ['dropdown-item'],
179 | content: 'Dropdown item'
180 | };
181 | const divider = {
182 | type: 'default',
183 | classes: ['dropdown-divider']
184 | };
185 | this.append(header);
186 | this.append(link);
187 | this.append(divider);
188 | this.append(link);
189 | }
190 | },
191 | isComponent(el) {
192 | if (el && el.classList && el.classList.contains('dropdown-menu')) {
193 | return { type: 'dropdown_menu' };
194 | }
195 | },
196 | view: defaultView,
197 | });
198 |
199 | }
200 |
--------------------------------------------------------------------------------
/src/components/FileInput.js:
--------------------------------------------------------------------------------
1 | import { elHasClass } from "../utils";
2 | import fileInputIcon from "raw-loader!../icons/file-input.svg";
3 |
4 | export const FileInputBlock = (bm, label) => {
5 | bm.add('file-input', {
6 | label: `
7 | ${fileInputIcon}
8 | ${label}
9 | `,
10 | category: 'Forms',
11 | content: ` `
12 | });
13 | };
14 |
15 | export default (dc, traits, config = {}) => {
16 | const defaultType = dc.getType('default');
17 | const defaultModel = defaultType.model;
18 | const defaultView = defaultType.view;
19 | const type = 'file-input';
20 |
21 | dc.addType(type, {
22 | model: {
23 | defaults: {
24 | ...defaultModel.prototype.defaults,
25 | 'custom-name': config.labels.input,
26 | tagName: 'input',
27 | draggable: 'form .form-group',
28 | droppable: false,
29 | traits: [
30 | traits.name,
31 | traits.required,
32 | {
33 | type: 'checkbox',
34 | label: config.labels.trait_multiple,
35 | name: 'multiple',
36 | },
37 | ],
38 | },
39 | },
40 | isComponent(el) {
41 | if (el.tagName === 'INPUT' && elHasClass(el, 'form-control-file')) {
42 | return { type };
43 | }
44 | },
45 | view: defaultView,
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Form.js:
--------------------------------------------------------------------------------
1 | import formIcon from "raw-loader!../icons/form.svg";
2 |
3 | export const FormBlock = (bm, label) => {
4 | bm.add('form', {
5 | label: `
6 | ${formIcon}
7 | ${label}
`,
8 | category: 'Forms',
9 | content: `
10 |
35 | `,
36 | });
37 | };
38 |
39 | export default (dc, traits, config = {}) => {
40 | const defaultType = dc.getType('default');
41 | const defaultModel = defaultType.model;
42 | const defaultView = defaultType.view;
43 | let actionTrait;
44 |
45 | // If the formPredefinedActions is set in the config you can add a dropdown menu to the actions trait
46 | if (config.formPredefinedActions && config.formPredefinedActions.length) {
47 | actionTrait = {
48 | type: 'select',
49 | label: config.labels.trait_action,
50 | name: 'action',
51 | options: [],
52 | };
53 | config.formPredefinedActions.forEach((action) => {
54 | actionTrait.options.push({ value: action.value, name: action.name })
55 | });
56 | } else {
57 | actionTrait = {
58 | label: config.labels.trait_action,
59 | name: 'action',
60 | }
61 | }
62 |
63 | dc.addType('form', {
64 | model: {
65 | defaults: {
66 | ...defaultModel.prototype.defaults,
67 | droppable: ':not(form)',
68 | draggable: ':not(form)',
69 | traits: [
70 | {
71 | type: 'select',
72 | label: config.labels.trait_enctype,
73 | name: 'enctype',
74 | options: [
75 | { value: 'application/x-www-form-urlencoded', name: 'application/x-www-form-urlencoded (default)' },
76 | { value: 'multipart/form-data', name: 'multipart/form-data' },
77 | { value: 'text/plain', name: 'text/plain' },
78 | ]
79 | },
80 | {
81 | type: 'select',
82 | label: config.labels.trait_method,
83 | name: 'method',
84 | options: [
85 | { value: 'post', name: 'POST' },
86 | { value: 'get', name: 'GET' },
87 | ]
88 | },
89 | actionTrait
90 | ],
91 | },
92 |
93 | init() {
94 | this.listenTo(this, 'change:formState', this.updateFormState);
95 | },
96 |
97 | updateFormState() {
98 | var state = this.get('formState');
99 | switch (state) {
100 | case 'success':
101 | this.showState('success');
102 | break;
103 | case 'error':
104 | this.showState('error');
105 | break;
106 | default:
107 | this.showState('normal');
108 | }
109 | },
110 |
111 | showState(state) {
112 | var st = state || 'normal';
113 | var failVis, successVis;
114 | if (st === 'success') {
115 | failVis = 'none';
116 | successVis = 'block';
117 | } else if (st === 'error') {
118 | failVis = 'block';
119 | successVis = 'none';
120 | } else {
121 | failVis = 'none';
122 | successVis = 'none';
123 | }
124 | var successModel = this.getStateModel('success');
125 | var failModel = this.getStateModel('error');
126 | var successStyle = successModel.getStyle();
127 | var failStyle = failModel.getStyle();
128 | successStyle.display = successVis;
129 | failStyle.display = failVis;
130 | successModel.setStyle(successStyle);
131 | failModel.setStyle(failStyle);
132 | },
133 |
134 | getStateModel(state) {
135 | var st = state || 'success';
136 | var stateName = 'form-state-' + st;
137 | var stateModel;
138 | var comps = this.get('components');
139 | for (var i = 0; i < comps.length; i++) {
140 | var model = comps.models[i];
141 | if (model.get('form-state-type') === st) {
142 | stateModel = model;
143 | break;
144 | }
145 | }
146 | if (!stateModel) {
147 | var contentStr = formMsgSuccess;
148 | if (st === 'error') {
149 | contentStr = formMsgError;
150 | }
151 | stateModel = comps.add({
152 | 'form-state-type': st,
153 | type: 'text',
154 | removable: false,
155 | copyable: false,
156 | draggable: false,
157 | attributes: { 'data-form-state': st },
158 | content: contentStr,
159 | });
160 | }
161 | return stateModel;
162 | },
163 | },
164 | isComponent(el) {
165 | if (el.tagName === 'FORM') {
166 | return { type: 'form' };
167 | }
168 | },
169 |
170 | view: {
171 | events: {
172 | submit(e) {
173 | e.preventDefault();
174 | }
175 | }
176 | },
177 | });
178 | }
179 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import headingIcon from "raw-loader!../icons/heading-solid.svg";
2 |
3 | export const HeaderBlock = (bm, label) => {
4 | bm.add('header', {
5 | label: `
6 | ${headingIcon}
7 | ${label}
8 | `,
9 | category: 'Typography',
10 | content: {
11 | type: 'header',
12 | content: 'Bootstrap heading'
13 | }
14 | });
15 | };
16 |
17 | export default (domc) => {
18 | const textType = domc.getType('text');
19 | const textModel = textType.model;
20 | const textView = textType.view;
21 |
22 | domc.addType('header', {
23 | extend: 'text',
24 | model: {
25 | defaults: Object.assign({}, textModel.prototype.defaults, {
26 | 'custom-name': 'Header',
27 | tagName: 'h1',
28 | traits: [
29 | {
30 | type: 'select',
31 | options: [
32 | { value: 'h1', name: 'One (largest)' },
33 | { value: 'h2', name: 'Two' },
34 | { value: 'h3', name: 'Three' },
35 | { value: 'h4', name: 'Four' },
36 | { value: 'h5', name: 'Five' },
37 | { value: 'h6', name: 'Six (smallest)' },
38 | ],
39 | label: 'Size',
40 | name: 'tagName',
41 | changeProp: 1
42 | },
43 | {
44 | type: 'class_select',
45 | options: [
46 | { value: '', name: 'None' },
47 | { value: 'display-1', name: 'One (largest)' },
48 | { value: 'display-2', name: 'Two ' },
49 | { value: 'display-3', name: 'Three ' },
50 | { value: 'display-4', name: 'Four (smallest)' }
51 | ],
52 | label: 'Display Heading'
53 | }
54 | ].concat(textModel.prototype.defaults.traits)
55 | }),
56 |
57 | },
58 | isComponent(el) {
59 | if (el && ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(el.tagName)) {
60 | return { type: 'header' };
61 | }
62 | },
63 | view: textView
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/Image.js:
--------------------------------------------------------------------------------
1 | import imageIcon from "raw-loader!../icons/image-solid.svg";
2 |
3 | export const ImageBlock = (bm, label) => {
4 | bm.add('bs-image', {
5 | label: `
6 | ${imageIcon}
7 | ${label}
8 | `,
9 | category: 'Media',
10 | content: {
11 | type: 'bs-image'
12 | }
13 | });
14 | };
15 |
16 | export default (domComponent) => {
17 | const img_src_default = 'https://dummyimage.com/800x500/999/222';
18 | const imageType = domComponent.getType('image');
19 | const model = imageType.model;
20 | const view = imageType.view;
21 | const type = 'bs-image';
22 |
23 | domComponent.addType(type, {
24 | extend: 'image',
25 | model: {
26 | defaults: Object.assign({}, model.prototype.defaults, {
27 | 'custom-name': 'Image',
28 | tagName: 'img',
29 | resizable: 1,
30 | attributes: {
31 | src: img_src_default,
32 | },
33 | classes: ['img-fluid'],
34 | traits: [
35 | {
36 | type: 'text',
37 | label: 'Source (URL)',
38 | name: 'src'
39 | },
40 | {
41 | type: 'text',
42 | label: 'Alternate text',
43 | name: 'alt'
44 | }
45 | ].concat(model.prototype.defaults.traits)
46 | })
47 | },
48 | isComponent: function (el) {
49 | if (el && el.tagName === 'IMG') {
50 | return { type: type };
51 | }
52 | },
53 | view: view
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/Input.js:
--------------------------------------------------------------------------------
1 | import inputIcon from "raw-loader!../icons/input.svg";
2 |
3 | export const InputBlock = (bm, label) => {
4 | bm.add('input', {
5 | label: `
6 | ${inputIcon}
7 | ${label}
`,
8 | category: 'Forms',
9 | content: ' ',
10 | });
11 | };
12 |
13 | export default (dc, traits, config = {}) => {
14 | const defaultType = dc.getType('default');
15 | const defaultModel = defaultType.model;
16 | const defaultView = defaultType.view;
17 |
18 | dc.addType('input', {
19 | model: {
20 | defaults: {
21 | ...defaultModel.prototype.defaults,
22 | 'custom-name': config.labels.input,
23 | tagName: 'input',
24 | draggable: 'form .form-group',
25 | droppable: false,
26 | traits: [
27 | traits.value,
28 | traits.name,
29 | traits.placeholder, {
30 | label: config.labels.trait_type,
31 | type: 'select',
32 | name: 'type',
33 | options: [
34 | { value: 'text', name: config.labels.type_text },
35 | { value: 'email', name: config.labels.type_email },
36 | { value: 'password', name: config.labels.type_password },
37 | { value: 'number', name: config.labels.type_number },
38 | { value: 'date', name: config.labels.type_date },
39 | { value: 'hidden', name: config.labels.type_hidden },
40 | ]
41 | }, traits.required
42 | ],
43 | },
44 | },
45 | isComponent(el) {
46 | if (el.tagName === 'INPUT') {
47 | return { type: 'input' };
48 | }
49 | },
50 | view: defaultView,
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/InputGroup.js:
--------------------------------------------------------------------------------
1 | import formGroupIcon from "raw-loader!../icons/form-group.svg";
2 | import inputGroupIcon from "raw-loader!../icons/input-group.svg";
3 |
4 | export const InputGroupBlock = (bm, label, c) => {
5 | bm.add('form_group_input', {
6 | label: `
7 | ${formGroupIcon}
8 | ${label}
`,
9 | category: 'Forms',
10 | content: `
11 |
12 | Name
13 |
14 |
15 | `,
16 | });
17 |
18 | bm.add('input_group', {
19 | label: `
20 | ${inputGroupIcon}
21 | ${label}
`,
22 | category: 'Forms',
23 | content: `
24 |
33 | `,
34 | });
35 | };
36 |
37 | export default (dc, traits, config = {}) => {
38 | const defaultType = dc.getType('default');
39 | const defaultModel = defaultType.model;
40 | const defaultView = defaultType.view;
41 |
42 | dc.addType('input_group', {
43 | model: {
44 | defaults: {
45 | ...defaultModel.prototype.defaults,
46 | 'custom-name': config.labels.input_group,
47 | tagName: 'div',
48 | traits: [],
49 | },
50 | },
51 | isComponent(el) {
52 | if (el && el.classList && el.classList.contains('form_group_input')) {
53 | return { type: 'form_group_input' };
54 | }
55 | },
56 | view: defaultView,
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/Label.js:
--------------------------------------------------------------------------------
1 | import labelIcon from "raw-loader!../icons/label.svg";
2 |
3 | export const LabelBlock = (bm, label) => {
4 | bm.add('label', {
5 | label: `
6 | ${labelIcon}
7 | ${label}
`,
8 | category: 'Forms',
9 | content: 'Label ',
10 | });
11 | };
12 |
13 | export default (dc, traits, config = {}) => {
14 |
15 | const textType = dc.getType('text');
16 | const textModel = textType.model;
17 | const textView = textType.view;
18 |
19 | dc.addType('label', {
20 | extend: 'text',
21 | model: {
22 | defaults: {
23 | ...textModel.prototype.defaults,
24 | 'custom-name': config.labels.label,
25 | tagName: 'label',
26 | traits: [traits.for],
27 | },
28 | },
29 | isComponent(el) {
30 | if (el.tagName == 'LABEL') {
31 | return { type: 'label' };
32 | }
33 | },
34 | view: textView,
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Link.js:
--------------------------------------------------------------------------------
1 | /*
2 | known issues:
3 | - BS dropdown JS isn't attached if you remove the existing toggle and add a new one
4 | */
5 |
6 | import linkIcon from "raw-loader!../icons/link-solid.svg";
7 |
8 | export const LinkBlock = (bm, label) => {
9 | bm.add('link', {
10 | label: `
11 | ${linkIcon}
12 | ${label}
13 | `,
14 | category: 'Basic',
15 | content: {
16 | type: 'link',
17 | content: 'Link text'
18 | }
19 | });
20 | };
21 |
22 | export default (editor) => {
23 | const comps = editor.DomComponents;
24 | const textType = comps.getType('text');
25 | const textModel = textType.model;
26 |
27 | const linkType = comps.getType('link');
28 | const linkView = linkType.view;
29 |
30 | comps.addType('link', {
31 | extend: 'text',
32 | model: {
33 | defaults: Object.assign({}, textModel.prototype.defaults, {
34 | 'custom-name': 'Link',
35 | tagName: 'a',
36 | droppable: true,
37 | editable: true,
38 | traits: [
39 | {
40 | type: 'text',
41 | label: 'Href',
42 | name: 'href',
43 | placeholder: 'https://www.grapesjs.com'
44 | },
45 | {
46 | type: 'select',
47 | options: [
48 | { value: '', name: 'This window' },
49 | { value: '_blank', name: 'New window' }
50 | ],
51 | label: 'Target',
52 | name: 'target',
53 | },
54 | {
55 | type: 'select',
56 | options: [
57 | { value: '', name: 'None' },
58 | { value: 'button', name: 'Self' },
59 | { value: 'collapse', name: 'Collapse' },
60 | { value: 'dropdown', name: 'Dropdown' }
61 | ],
62 | label: 'Toggles',
63 | name: 'data-toggle',
64 | changeProp: 1
65 | }
66 | ].concat(textModel.prototype.defaults.traits)
67 | }),
68 | init2() {
69 | //textModel.prototype.init.call(this);
70 | this.listenTo(this, 'change:data-toggle', this.setupToggle);
71 | this.listenTo(this, 'change:attributes', this.setupToggle); // for when href changes
72 | },
73 | setupToggle(a, b, options = {}) { // TODO this should be in the dropdown comp and not the link comp
74 | if (options.ignore === true && options.force !== true) {
75 | return;
76 | }
77 | console.log('setup toggle');
78 | const attrs = this.getAttributes();
79 | const href = attrs.href;
80 | // old attributes are not removed from DOM even if deleted...
81 | delete attrs['data-toggle'];
82 | delete attrs['aria-expanded'];
83 | delete attrs['aria-controls'];
84 | delete attrs['aria-haspopup'];
85 | if (href && href.length > 0 && href.match(/^#/)) {
86 | console.log('link has href');
87 | // find the el where id == link href
88 | const els = this.em.get('Editor').DomComponents.getWrapper().find(href);
89 | if (els.length > 0) {
90 | console.log('referenced el found');
91 | const el = els[0]; // should only be one el with this ID
92 | const el_attrs = el.getAttributes();
93 | //delete el_attrs['aria-labelledby'];
94 | const el_classes = el_attrs.class;
95 | if (el_classes) {
96 | console.log('el has classes');
97 | const el_classes_list = el_classes.split(' ');
98 | const includes = ['collapse', 'dropdown-menu'];
99 | const intersection = el_classes_list.filter(x => includes.includes(x));
100 |
101 | if (intersection.length) {
102 | console.log('link data-toggle matches el class');
103 | switch (intersection[0]) {
104 | case 'collapse':
105 | attrs['data-toggle'] = 'collapse';
106 | break;
107 | }
108 | attrs['aria-expanded'] = el_classes_list.includes('show');
109 | if (intersection[0] === 'collapse') {
110 | attrs['aria-controls'] = href.substring(1);
111 | }
112 | }
113 | }
114 | }
115 | }
116 | this.set('attributes', attrs, { ignore: true });
117 | },
118 | classesChanged(e) {
119 | console.log('classes changed');
120 | if (this.attributes.type === 'link') {
121 | if (this.attributes.classes.filter(function (klass) {
122 | return klass.id === 'btn'
123 | }).length > 0) {
124 | this.changeType('button');
125 | }
126 | }
127 | }
128 | },
129 | isComponent(el) {
130 | if (el && el.tagName && el.tagName === 'A') {
131 | return { type: 'link' };
132 | }
133 | },
134 | view: linkView
135 | });
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/src/components/List.js:
--------------------------------------------------------------------------------
1 | export const ListBlock = (bm, label) => {
2 | bm.add('list', {
3 | label: label,
4 | category: 'Basic',
5 | attributes: { class: 'fa fa-list' },
6 | content: {
7 | type: 'list'
8 | }
9 | });
10 | };
11 |
12 | export default (domc) => {
13 | const defaultType = domc.getType('default');
14 | const defaultModel = defaultType.model;
15 | const defaultView = defaultType.view;
16 |
17 | domc.addType('list', {
18 | model: {
19 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
20 | 'custom-name': 'List',
21 | tagName: 'ul',
22 | resizable: 1,
23 | traits: [
24 | {
25 | type: 'select',
26 | options: [
27 | { value: 'ul', name: 'No' },
28 | { value: 'ol', name: 'Yes' }
29 | ],
30 | label: 'Ordered?',
31 | name: 'tagName',
32 | changeProp: 1
33 | }
34 | ].concat(defaultModel.prototype.defaults.traits)
35 | })
36 | },
37 | isComponent: function (el) {
38 | if (el && ['UL', 'OL'].includes(el.tagName)) {
39 | return { type: 'list' };
40 | }
41 | },
42 | view: defaultView
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/MediaObject.js:
--------------------------------------------------------------------------------
1 | import columnsIcon from 'raw-loader!../icons/columns-solid.svg';
2 |
3 | export const MediaObjectBlock = (bm, label) => {
4 | bm.add('media_object').set({
5 | label: `
6 | ${columnsIcon}
7 | ${label}
8 | `,
9 | category: 'Layout',
10 | content: ``
17 | });
18 | };
19 |
20 | export default (domc) => {
21 | const defaultType = domc.getType('default');
22 | const defaultModel = defaultType.model;
23 | const defaultView = defaultType.view;
24 |
25 | domc.addType('media_object', {
26 | model: {
27 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
28 | 'custom-name': 'Media Object',
29 | tagName: 'div',
30 | classes: ['media']
31 | })
32 | },
33 | isComponent(el) {
34 | if (el && el.classList && el.classList.contains('media')) {
35 | return { type: 'media' };
36 | }
37 | },
38 | view: defaultView
39 | });
40 |
41 | domc.addType('media_body', {
42 | model: {
43 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
44 | 'custom-name': 'Media Body',
45 | tagName: 'div',
46 | classes: ['media-body']
47 | })
48 | },
49 | isComponent(el) {
50 | if (el && el.classList && el.classList.contains('media-body')) {
51 | return { type: 'media_body' };
52 | }
53 | },
54 | view: defaultView
55 | });
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/Paragraph.js:
--------------------------------------------------------------------------------
1 | import paragraphIcon from "raw-loader!../icons/paragraph-solid.svg";
2 |
3 | export const ParagraphBlock = (bm, label) => {
4 | bm.add('paragraph', {
5 | label: `
6 | ${paragraphIcon}
7 | ${label}
8 | `,
9 | category: 'Typography',
10 | content: {
11 | type: 'paragraph',
12 | content: 'Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus.'
13 | }
14 | });
15 | };
16 |
17 | export default (domc) => {
18 | const textType = domc.getType('text');
19 | const textModel = textType.model;
20 | const textView = textType.view;
21 |
22 | domc.addType('paragraph', {
23 | extend: 'text',
24 | model: {
25 | defaults: Object.assign({}, textModel.prototype.defaults, {
26 | 'custom-name': 'Paragraph',
27 | tagName: 'p',
28 | traits: [
29 | {
30 | type: 'class_select',
31 | options: [
32 | { value: '', name: 'No' },
33 | { value: 'lead', name: 'Yes' }
34 | ],
35 | label: 'Lead?'
36 | }
37 | ].concat(textModel.prototype.defaults.traits)
38 | })
39 | },
40 | isComponent(el) {
41 | if (el && el.tagName && el.tagName === 'P') {
42 | return { type: 'paragraph' };
43 | }
44 | },
45 | view: textView
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Radio.js:
--------------------------------------------------------------------------------
1 | import radioIcon from "raw-loader!../icons/dot-circle-regular.svg";
2 |
3 | export const RadioBlock = (bm, label) => {
4 | bm.add('radio', {
5 | label: `
6 | ${radioIcon}
7 | ${label}
8 | `,
9 | category: 'Forms',
10 | content: `
11 |
12 |
13 |
14 | Default radio
15 |
16 |
17 | `,
18 | });
19 | };
20 |
21 | export default (dc, traits, config = {}) => {
22 | const checkType = dc.getType('checkbox');
23 |
24 | // RADIO
25 | dc.addType('radio', {
26 | extend: 'checkbox',
27 | model: {
28 | defaults: {
29 | ...checkType.model.prototype.defaults,
30 | 'custom-name': config.labels.radio,
31 | attributes: { type: 'radio' },
32 | },
33 | },
34 | isComponent(el) {
35 | if (el.tagName === 'INPUT' && el.type === 'radio') {
36 | return { type: 'radio' };
37 | }
38 | },
39 | view: checkType.view,
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/Row.js:
--------------------------------------------------------------------------------
1 | import windowIcon from "raw-loader!../icons/window-maximize-solid.svg";
2 |
3 | export const RowBlock = (bm, label) => {
4 | bm.add('row').set({
5 | label: `
6 | ${windowIcon}
7 | ${label}
8 | `,
9 | category: 'Layout',
10 | content: {
11 | type: 'row',
12 | classes: ['row']
13 | }
14 | });
15 | };
16 |
17 | export default (domc) => {
18 | const defaultType = domc.getType('default');
19 | const defaultModel = defaultType.model;
20 | const defaultView = defaultType.view;
21 |
22 | domc.addType('row', {
23 | model: {
24 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
25 | 'custom-name': 'Row',
26 | tagName: 'div',
27 | draggable: '.container, .container-fluid',
28 | droppable: true,
29 | traits: [
30 | {
31 | type: 'class_select',
32 | options: [
33 | { value: '', name: 'Yes' },
34 | { value: 'no-gutters', name: 'No' }
35 | ],
36 | label: 'Gutters?'
37 | }
38 | ].concat(defaultModel.prototype.defaults.traits)
39 | })
40 | },
41 | isComponent(el) {
42 | if (el && el.classList && el.classList.contains('row')) {
43 | return { type: 'row' };
44 | }
45 | },
46 | view: defaultView
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Select.js:
--------------------------------------------------------------------------------
1 | import selectIcon from "raw-loader!../icons/select-input.svg";
2 |
3 | export const SelectBlock = (bm, label) => {
4 | bm.add('select', {
5 | label: `
6 | ${selectIcon}
7 | ${label}
`,
8 | category: 'Forms',
9 | content: `
10 | ${label ? `${label} ` : ''}
11 | ${label} 1
12 | `,
13 | });
14 | };
15 |
16 | export default (editor, dc, traits, config = {}) => {
17 | const defaultType = dc.getType('default');
18 | const defaultModel = defaultType.model;
19 | const inputType = dc.getType('input');
20 | const inputModel = inputType.model;
21 |
22 | const preventDefaultClick = () => {
23 | return defaultType.view.extend({
24 | events: {
25 | 'mousedown': 'handleClick',
26 | },
27 |
28 | handleClick(e) {
29 | e.preventDefault();
30 | },
31 | });
32 | };
33 |
34 | // SELECT
35 | dc.addType('select', {
36 | model: {
37 | defaults: {
38 | ...inputModel.prototype.defaults,
39 | 'custom-name': config.labels.select,
40 | tagName: 'select',
41 | traits: [
42 | traits.name, {
43 | label: config.labels.trait_options,
44 | type: 'select-options'
45 | },
46 | traits.required
47 | ],
48 | },
49 | },
50 | isComponent(el) {
51 | if (el.tagName === 'SELECT') {
52 | return { type: 'select' };
53 | }
54 | },
55 | view: preventDefaultClick(),
56 | });
57 |
58 | const traitManager = editor.TraitManager;
59 | traitManager.addType('select-options', {
60 | events: {
61 | 'keyup': 'onChange',
62 | },
63 |
64 | onValueChange: function () {
65 | const optionsStr = this.model.get('value').trim();
66 | const options = optionsStr.split('\n');
67 | const optComps = [];
68 |
69 | for (let i = 0; i < options.length; i++) {
70 | const optionStr = options[i];
71 | const option = optionStr.split(config.optionsStringSeparator);
72 | const opt = {
73 | tagName: 'option',
74 | attributes: {}
75 | };
76 | if (option[1]) {
77 | opt.content = option[1];
78 | opt.attributes.value = option[0];
79 | } else {
80 | opt.content = option[0];
81 | opt.attributes.value = option[0];
82 | }
83 | optComps.push(opt);
84 | }
85 |
86 | const comps = this.target.get('components');
87 | comps.reset(optComps);
88 | this.target.view.render();
89 | },
90 |
91 | getInputEl: function () {
92 | if (!this.$input) {
93 | const target = this.target;
94 | let optionsStr = '';
95 | const options = target.get('components');
96 |
97 | for (let i = 0; i < options.length; i++) {
98 | const option = options.models[i];
99 | const optAttr = option.get('attributes');
100 | const optValue = optAttr.value || '';
101 | optionsStr += `${optValue}${config.optionsStringSeparator}${option.get('content')}\n`;
102 | }
103 |
104 | this.$input = document.createElement('textarea');
105 | this.$input.value = optionsStr;
106 | }
107 | return this.$input;
108 | },
109 | });
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/Text.js:
--------------------------------------------------------------------------------
1 | import fontIcon from "raw-loader!../icons/font-solid.svg";
2 |
3 | export const TextBlock = (bm, label) => {
4 | bm.add('text', {
5 | label: `
6 | ${fontIcon}
7 | ${label}
8 | `,
9 | category: 'Typography',
10 | content: {
11 | type: 'text',
12 | content: 'Insert your text here'
13 | }
14 | });
15 | };
16 |
17 | export default (domc) => {
18 | const defaultType = domc.getType('default');
19 | const defaultModel = defaultType.model;
20 | const textType = domc.getType('text');
21 | const textView = textType.view;
22 |
23 | domc.addType('text', {
24 | model: {
25 | defaults: Object.assign({}, defaultModel.prototype.defaults, {
26 | 'custom-name': 'Text',
27 | tagName: 'div',
28 | droppable: true,
29 | editable: true
30 | })
31 | },
32 | view: textView
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/Textarea.js:
--------------------------------------------------------------------------------
1 | import textareaIcon from "raw-loader!../icons/textarea.svg";
2 |
3 | export const TextareaBlock = (bm, label) => {
4 | bm.add('textarea', {
5 | label: `
6 | ${textareaIcon}
7 | ${label}
`,
8 | category: 'Forms',
9 | content: ' ',
10 | });
11 | };
12 |
13 | export default (dc, traits, config = {}) => {
14 | const defaultType = dc.getType('default');
15 | const defaultView = defaultType.view;
16 | const inputType = dc.getType('input');
17 | const inputModel = inputType.model;
18 |
19 | // TEXTAREA
20 | dc.addType('textarea', {
21 | extend: 'input',
22 | model: {
23 | defaults: {
24 | ...inputModel.prototype.defaults,
25 | 'custom-name': config.labels.textarea,
26 | tagName: 'textarea',
27 | traits: [
28 | traits.name,
29 | traits.placeholder,
30 | traits.required
31 | ]
32 | },
33 | },
34 | isComponent(el) {
35 | if (el.tagName === 'TEXTAREA') {
36 | return { type: 'textarea' };
37 | }
38 | },
39 | view: defaultView,
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/tabs/Tab.js:
--------------------------------------------------------------------------------
1 | import constants from './constants';
2 | import { elHasClass } from '../../utils';
3 |
4 | export default (dc, config = {}) => {
5 | const defaultType = dc.getType('default');
6 | const defaultModel = defaultType.model;
7 | const defaultView = defaultType.view;
8 | const { tabName, navigationSelector } = constants;
9 | const classId = config.classTab;
10 | const type = tabName;
11 |
12 | dc.addType(type, {
13 |
14 |
15 | model: {
16 | defaults: {
17 | ...defaultModel.prototype.defaults,
18 | name: 'Tab',
19 | tagName: 'li',
20 | copyable: true,
21 | draggable: navigationSelector,
22 |
23 | },
24 |
25 | init() {
26 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId);
27 | }
28 | },
29 | isComponent(el) {
30 | if (elHasClass(el, classId)) return { type };
31 | },
32 |
33 | view: {
34 | init() {
35 | const comps = this.model.components();
36 |
37 | // Add a basic template if it's not yet initialized
38 | if (!comps.length) {
39 | comps.add(`
40 | Tab
41 | `);
42 | }
43 | },
44 | },
45 | });
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/tabs/TabPane.js:
--------------------------------------------------------------------------------
1 | import constants from './constants';
2 | import { elHasClass } from '../../utils';
3 |
4 | export default (dc, config = {}) => {
5 | const defaultType = dc.getType('default');
6 | const defaultModel = defaultType.model;
7 | const defaultView = defaultType.view;
8 | const { tabPaneName, tabPanesSelector } = constants;
9 | const classId = config.classTabPane;
10 | const type = tabPaneName;
11 |
12 | dc.addType(type, {
13 |
14 | model: {
15 | defaults: {
16 | ...defaultModel.prototype.defaults,
17 | name: 'Tab Pane',
18 | copyable: true,
19 | draggable: tabPanesSelector,
20 |
21 | traits: [
22 | 'id',
23 | {
24 | type: 'class_select',
25 | options: [
26 | { value: 'fade', name: 'Fade' },
27 | { value: '', name: 'None' },
28 | ],
29 | label: 'Animation',
30 | },
31 | {
32 | type: 'class_select',
33 | options: [
34 | { value: '', name: 'Inactive' },
35 | { value: 'active', name: 'Active' },
36 | ],
37 | label: 'Is Active',
38 | },
39 | ],
40 | },
41 |
42 | init() {
43 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId);
44 | }
45 | },
46 | isComponent(el) {
47 | if (elHasClass(el, classId)) return { type };
48 | },
49 |
50 | view: defaultView,
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/tabs/TabsNavigation.js:
--------------------------------------------------------------------------------
1 | import constants from './constants';
2 | import { elHasClass } from '../../utils';
3 | import ellipsisIcon from "raw-loader!../../icons/ellipsis-h-solid.svg";
4 | import circleIcon from "raw-loader!../../icons/circle-solid.svg";
5 | import windowIcon from "raw-loader!../../icons/window-maximize-solid.svg";
6 |
7 | export const TabsBlock = (bm, c) => {
8 | bm.add('tabs', {
9 | label: `
10 | ${ellipsisIcon}
11 | ${c.labels.tabs}
12 | `,
13 | category: 'Components',
14 | content: `
15 |
26 |
31 | `
32 | });
33 | bm.add('tabs-tab', {
34 | label: `
35 | ${circleIcon}
36 | ${c.labels.tab}
37 | `,
38 | category: 'Components',
39 | content: {
40 | type: 'tabs-tab',
41 | }
42 | });
43 | bm.add('tabs-tab-pane', {
44 | label: `
45 | ${windowIcon}
46 | ${c.labels.tabPane}
47 | `,
48 | category: 'Components',
49 | content: {
50 | type: 'tabs-tab-pane',
51 | }
52 | });
53 | };
54 |
55 | export default (dc, config = {}) => {
56 | const defaultType = dc.getType('default');
57 | const defaultModel = defaultType.model;
58 | const defaultView = defaultType.view;
59 | const { navigationName, tabSelector } = constants;
60 | const classId = config.classNavigation;
61 | const type = navigationName;
62 |
63 | dc.addType(type, {
64 |
65 | model: {
66 | defaults: {
67 | ...defaultModel.prototype.defaults,
68 | name: 'Tabs Navigation',
69 | copyable: 0,
70 | draggable: true,
71 | droppable: tabSelector,
72 |
73 | traits: [
74 | {
75 | type: 'class_select',
76 | options: [
77 | { value: 'nav-tabs', name: 'Tabs' },
78 | { value: 'nav-pills', name: 'Pills' },
79 | ],
80 | label: 'Type',
81 | },
82 | {
83 | type: 'class_select',
84 | options: [
85 | { value: '', name: 'Left' },
86 | { value: 'nav-fill', name: 'Fill' },
87 | { value: 'nav-justified', name: 'Justify' },
88 | ],
89 | label: 'Layout',
90 | },
91 | ],
92 | },
93 |
94 | init() {
95 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId);
96 | }
97 | },
98 | isComponent(el) {
99 | if (elHasClass(el, classId)) return { type };
100 | },
101 |
102 | view: {
103 | init() {
104 | const props = [
105 | 'type',
106 | 'layout',
107 | ];
108 | const reactTo = props.map(prop => `change:${prop}`).join(' ');
109 | this.listenTo(this.model, reactTo, this.render);
110 | const comps = this.model.components();
111 |
112 | // Add a basic template if it's not yet initialized
113 | if (!comps.length) {
114 | comps.add(`
115 |
116 |
117 | Tab 1
118 |
119 |
120 | Tab 2
121 |
122 |
123 | Tab 3
124 |
125 |
126 | `);
127 | }
128 | },
129 | },
130 | });
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/tabs/TabsPanes.js:
--------------------------------------------------------------------------------
1 | import constants from './constants';
2 | import { elHasClass } from '../../utils';
3 |
4 | export default (dc, config = {}) => {
5 | const defaultType = dc.getType('default');
6 | const defaultModel = defaultType.model;
7 | const defaultView = defaultType.view;
8 | const { tabPanesName, tabPaneSelector } = constants;
9 | const classId = config.classTabPanes;
10 | const type = tabPanesName;
11 |
12 | dc.addType(type, {
13 |
14 | model: {
15 | defaults: {
16 | ...defaultModel.prototype.defaults,
17 | name: 'Tabs Panes',
18 | copyable: 0,
19 | draggable: true,
20 | droppable: tabPaneSelector,
21 | },
22 |
23 | init() {
24 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId);
25 | }
26 | },
27 | isComponent(el) {
28 | if (elHasClass(el, classId)) return { type };
29 | },
30 |
31 | view: {
32 | init() {
33 | const comps = this.model.components();
34 |
35 | // Add a basic template if it's not yet initialized
36 | if (!comps.length) {
37 | comps.add(`
38 |
39 |
Tab pane 1
40 |
Tab pane 2
41 |
Tab pane 3
42 |
43 | `);
44 | }
45 | },
46 | },
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/tabs/constants.js:
--------------------------------------------------------------------------------
1 | const prefix = 'tabs-';
2 | const containerName = `${prefix}container`;
3 | const navigationName = `${prefix}navigation`;
4 | const tabPanesName = `${prefix}panes`;
5 | const tabName = `${prefix}tab`;
6 | const tabPaneName = `${prefix}tab-pane`;
7 |
8 | export default {
9 | navigationName,
10 | tabPanesName,
11 | tabName,
12 | tabPaneName,
13 |
14 | // Selectors
15 | navigationSelector: `[data-gjs-type="${navigationName}"]`,
16 | tabPanesSelector: `[data-gjs-type="${tabPanesName}"]`,
17 | tabSelector: `[data-gjs-type="${tabName}"]`,
18 | tabPaneSelector: `[data-gjs-type="${tabPaneName}"]`,
19 |
20 | // IDs
21 | containerId: `data-${containerName}`,
22 | navigationId: `data-${navigationName}`,
23 | tabPanesId: `data-${tabPanesName}`,
24 | tabId: `data-${tabName}`,
25 | tabPaneId: `data-${tabPaneName}`,
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/video/Embed.js:
--------------------------------------------------------------------------------
1 | export default (domComponent) => {
2 | const src_default = 'https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4';
3 | const defaultType = domComponent.getType('default');
4 | const model = defaultType.model;
5 | const view = defaultType.view;
6 | const type = 'bs-video';
7 |
8 | domComponent.addType(type, {
9 | model: {
10 | defaults: Object.assign({}, model.prototype.defaults, {
11 | 'custom-name': 'Embed',
12 | tagName: 'div',
13 | resizable: false,
14 | droppable: false,
15 | classes: ['embed-responsive', 'embed-responsive-16by9'],
16 | traits: [
17 | {
18 | type: 'class_select',
19 | options: [
20 | { value: 'embed-responsive-21by9', name: '21:9' },
21 | { value: 'embed-responsive-16by9', name: '16:9' },
22 | { value: 'embed-responsive-4by3', name: '4:3' },
23 | { value: 'embed-responsive-1by1', name: '1:1' },
24 | ],
25 | label: 'Aspect Ratio',
26 | },
27 | ].concat(model.prototype.defaults.traits)
28 | })
29 | },
30 | isComponent: function (el) {
31 | if (el && el.className === 'embed-responsive') {
32 | return { type: type };
33 | }
34 | },
35 | view: {
36 | init() {
37 | const props = [
38 | 'Aspect Ratio',
39 | ];
40 | const reactTo = props.map(prop => `change:${prop}`).join(' ');
41 | this.listenTo(this.model, reactTo, this.render);
42 | const comps = this.model.components();
43 | // Add a basic template if it's not yet initialized
44 | if (!comps.length) {
45 | comps.add(``);
46 | }
47 | },
48 | },
49 | });
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/video/Video.js:
--------------------------------------------------------------------------------
1 | import videoIcon from "raw-loader!../../icons/youtube-brands.svg";
2 |
3 | export const VideoBlock = (bm, label) => {
4 | bm.add('bs-video', {
5 | label: `
6 | ${videoIcon}
7 | ${label}
8 | `,
9 | category: 'Media',
10 | content: {
11 | type: 'bs-video'
12 | }
13 | });
14 | };
15 |
16 | export default (domComponent) => {
17 | const videoType = domComponent.getType('video');
18 | const model = videoType.model;
19 | const view = videoType.view;
20 | const type = 'bs-embed-responsive';
21 |
22 | domComponent.addType(type, {
23 | extend: 'video',
24 | model: {
25 | defaults: Object.assign({}, model.prototype.defaults, {
26 | 'custom-name': 'Video',
27 | resizable: false,
28 | droppable: false,
29 | draggable: false,
30 | copyable: false,
31 | provider: 'so',
32 | classes: ['embed-responsive-item'],
33 | })
34 | },
35 | isComponent: function (el) {
36 | if (el && el.className === 'embed-responsive-item') {
37 | var result = {
38 | provider: 'so',
39 | type: type
40 | };
41 | var isYtProv = /youtube\.com\/embed/.test(el.src);
42 | var isYtncProv = /youtube-nocookie\.com\/embed/.test(el.src);
43 | var isViProv = /player\.vimeo\.com\/video/.test(el.src);
44 | var isExtProv = isYtProv || isYtncProv || isViProv;
45 | if (el.tagName == 'VIDEO' || (el.tagName == 'IFRAME' && isExtProv)) {
46 | if (el.src) result.src = el.src;
47 | if (isExtProv) {
48 | if (isYtProv) result.provider = 'yt';
49 | else if (isYtncProv) result.provider = 'ytnc';
50 | else if (isViProv) result.provider = 'vi';
51 | }
52 | }
53 | return result;
54 |
55 | }
56 | },
57 | view: view
58 | });
59 | }
60 |
--------------------------------------------------------------------------------
/src/devices.js:
--------------------------------------------------------------------------------
1 | export default (editor, config = {}) => {
2 | const c = config;
3 | const deviceManager = editor.DeviceManager;
4 | if(c.gridDevices) {
5 | deviceManager.add('Extra Small', '575px');
6 | deviceManager.add('Small', '767px');
7 | deviceManager.add('Medium', '991px');
8 | deviceManager.add('Large', '1199px');
9 | deviceManager.add('Extra Large');
10 |
11 |
12 | if(c.gridDevicesPanel) {
13 | const panels = editor.Panels;
14 | const commands = editor.Commands;
15 | var panelDevices = panels.addPanel({id: 'devices-buttons'});
16 | var deviceBtns = panelDevices.get('buttons');
17 | deviceBtns.add([{
18 | id: 'deviceXl',
19 | command: 'set-device-xl',
20 | className: 'fa fa-desktop',
21 | text: 'XL',
22 | attributes: {'title': 'Extra Large'},
23 | active: 1
24 | },{
25 | id: 'deviceLg',
26 | command: 'set-device-lg',
27 | className: 'fa fa-desktop',
28 | attributes: {'title': 'Large'}
29 | },{
30 | id: 'deviceMd',
31 | command: 'set-device-md',
32 | className: 'fa fa-tablet',
33 | attributes: {'title': 'Medium'}
34 | },{
35 | id: 'deviceSm',
36 | command: 'set-device-sm',
37 | className: 'fa fa-mobile',
38 | attributes: {'title': 'Small'}
39 | },{
40 | id: 'deviceXs',
41 | command: 'set-device-xs',
42 | className: 'fa fa-mobile',
43 | attributes: {'title': 'Extra Small'}
44 | }]);
45 |
46 | commands.add('set-device-xs', {
47 | run: function(editor) {
48 | editor.setDevice('Extra Small');
49 | }
50 | });
51 | commands.add('set-device-sm', {
52 | run: function(editor) {
53 | editor.setDevice('Small');
54 | }
55 | });
56 | commands.add('set-device-md', {
57 | run: function(editor) {
58 | editor.setDevice('Medium');
59 | }
60 | });
61 | commands.add('set-device-lg', {
62 | run: function(editor) {
63 | editor.setDevice('Large');
64 | }
65 | });
66 | commands.add('set-device-xl', {
67 | run: function(editor) {
68 | editor.setDevice('Extra Large');
69 | }
70 | });
71 | }
72 |
73 |
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/icons/button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/icons/caret-square-down-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/certificate-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/check-square-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/circle-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/columns-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/compress-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/credit-card-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/dot-circle-regular.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/ellipsis-h-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/equals-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/exclamation-triangle-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/file-input.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/icons/font-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/form-group.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/icons/form.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/icons/heading-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/image-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/image-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/input-group.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/icons/input.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/icons/label.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/icons/link-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/paragraph-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/select-input.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/icons/textarea.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/icons/window-maximize-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/icons/youtube-brands.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import grapesjs from 'grapesjs';
2 | import loadCommands from './commands';
3 | import loadTraits from './traits';
4 | import loadComponents from './components';
5 | import loadDevices from './devices';
6 |
7 | const loadCss = editor => {
8 | editor.Config.canvasCss += `
9 | /* Layout */
10 |
11 | .gjs-dashed .container, .gjs-dashed .container-fluid,
12 | .gjs-dashed .row,
13 | .gjs-dashed .col, .gjs-dashed [class^="col-"] {
14 | min-height: 1.5rem !important;
15 | }
16 | .gjs-dashed .w-100 {
17 | min-height: .25rem !important;
18 | background-color: rgba(0,0,0,0.1);
19 | }
20 | .gjs-dashed img {
21 | min-width: 25px;
22 | min-height: 25px;
23 | background-color: rgba(0,0,0,0.5);
24 | }
25 |
26 | /* Components */
27 |
28 | .gjs-dashed .btn-group,
29 | .gjs-dashed .btn-toolbar {
30 | padding-right: 1.5rem !important;
31 | min-height: 1.5rem !important;
32 | }
33 | .gjs-dashed .card,
34 | .gjs-dashed .card-group, .gjs-dashed .card-deck, .gjs-dashed .card-columns {
35 | min-height: 1.5rem !important;
36 | }
37 | .gjs-dashed .collapse {
38 | display: block !important;
39 | min-height: 1.5rem !important;
40 | }
41 | .gjs-dashed .dropdown {
42 | display: block !important;
43 | min-height: 1.5rem !important;
44 | }
45 | .gjs-dashed .dropdown-menu {
46 | min-height: 1.5rem !important;
47 | display: block !important;
48 | }
49 | `
50 | };
51 |
52 | export default grapesjs.plugins.add('grapesjs-blocks-bootstrap4', (editor, opts = {}) => {
53 |
54 | window.editor = editor;
55 |
56 | const opts_blocks = opts.blocks || {};
57 | const opts_labels = opts.labels || {};
58 | const opts_categories = opts.blockCategories || {};
59 | delete opts['blocks'];
60 | delete opts['labels'];
61 | delete opts['blockCategories'];
62 |
63 | const default_blocks = {
64 | default: true,
65 | text: true,
66 | link: true,
67 | image: true,
68 | // LAYOUT
69 | container: true,
70 | row: true,
71 | column: true,
72 | column_break: true,
73 | media_object: true,
74 | // COMPONENTS
75 | alert: true,
76 | tabs: true,
77 | badge: true,
78 | button: true,
79 | button_group: true,
80 | button_toolbar: true,
81 | card: true,
82 | card_container: true,
83 | collapse: true,
84 | dropdown: true,
85 | video: true,
86 | // TYPOGRAPHY
87 | header: true,
88 | paragraph: true,
89 | // BASIC
90 | list: true,
91 | // FORMS
92 | form: true,
93 | input: true,
94 | form_group_input: true,
95 | input_group: true,
96 | textarea: true,
97 | select: true,
98 | label: true,
99 | checkbox: true,
100 | radio: true,
101 | };
102 |
103 | const default_labels = {
104 | // LAYOUT
105 | container: 'Container',
106 | row: 'Row',
107 | column: 'Column',
108 | column_break: 'Column Break',
109 | media_object: 'Media Object',
110 |
111 | // COMPONENTS
112 | alert: 'Alert',
113 | tabs: 'Tabs',
114 | tab: 'Tab',
115 | tabPane: 'Tab Pane',
116 | badge: 'Badge',
117 | button: 'Button',
118 | button_group: 'Button Group',
119 | button_toolbar: 'Button Toolbar',
120 | card: 'Card',
121 | card_container: 'Card Container',
122 | collapse: 'Collapse',
123 | dropdown: 'Dropdown',
124 | dropdown_menu: 'Dropdown Menu',
125 | dropdown_item: 'Dropdown Item',
126 |
127 | // MEDIA
128 | image: 'Image',
129 | video: 'Video',
130 |
131 | // TYPOGRAPHY
132 | text: 'Text',
133 |
134 | // BASIC
135 | header: 'Header',
136 | paragraph: 'Paragraph',
137 | link: 'Link',
138 | list: 'Simple List',
139 |
140 | // FORMS
141 | form: 'Form',
142 | input: 'Input',
143 | file_input: 'File',
144 | form_group_input: 'Form Group',
145 | input_group: 'Input group',
146 | textarea: 'Textarea',
147 | select: 'Select',
148 | select_option: '- Select option -',
149 | option: 'Option',
150 | label: 'Label',
151 | checkbox: 'Checkbox',
152 | radio: 'Radio',
153 | trait_method: 'Method',
154 | trait_enctype: 'Encoding Type',
155 | trait_multiple: 'Multiple',
156 | trait_action: 'Action',
157 | trait_state: 'State',
158 | trait_id: 'ID',
159 | trait_for: 'For',
160 | trait_name: 'Name',
161 | trait_placeholder: 'Placeholder',
162 | trait_value: 'Value',
163 | trait_required: 'Required',
164 | trait_type: 'Type',
165 | trait_options: 'Options',
166 | trait_checked: 'Checked',
167 | type_text: 'Text',
168 | type_email: 'Email',
169 | type_password: 'Password',
170 | type_number: 'Number',
171 | type_date: 'Date',
172 | type_hidden: 'Hidden',
173 | type_submit: 'Submit',
174 | type_reset: 'Reset',
175 | type_button: 'Button',
176 | };
177 |
178 | const default_categories = {
179 | 'layout': true,
180 | 'media': true,
181 | 'components': true,
182 | 'typography': true,
183 | 'basic': true,
184 | 'forms': true,
185 | };
186 |
187 | let options = { ...{
188 | blocks: Object.assign(default_blocks, opts_blocks),
189 | labels: Object.assign(default_labels, opts_labels),
190 | blockCategories: Object.assign(default_categories, opts_categories),
191 | optionsStringSeparator: '::',
192 | gridDevices: true,
193 | gridDevicesPanel: false,
194 | classNavigation: 'nav',
195 | classTabPanes: 'tab-content',
196 | classTabPane: 'tab-pane',
197 | classTab: 'nav-item',
198 | }, ...opts };
199 |
200 | // Add components
201 | loadCommands(editor, options);
202 | loadTraits(editor, options);
203 | loadComponents(editor, options);
204 | loadDevices(editor, options);
205 | loadCss(editor, options);
206 | });
207 |
--------------------------------------------------------------------------------
/src/traits.js:
--------------------------------------------------------------------------------
1 | export default (editor, config = {}) => {
2 |
3 | const tm = editor.TraitManager;
4 |
5 | // Select trait that maps a class list to the select options.
6 | // The default select option is set if the input has a class, and class list is modified when select value changes.
7 | tm.addType('class_select', {
8 | events: {
9 | 'change': 'onChange' // trigger parent onChange method on input change
10 | },
11 | createInput({trait}) {
12 | const md = this.model;
13 | const opts = md.get('options') || [];
14 | const input = document.createElement('select');
15 | const target_view_el = this.target.view.el;
16 |
17 | for (let i = 0; i < opts.length; i++) {
18 | const option = document.createElement('option');
19 | let value = opts[i].value;
20 | if (value === '') {
21 | value = 'GJS_NO_CLASS';
22 | } // 'GJS_NO_CLASS' represents no class--empty string does not trigger value change
23 | option.text = opts[i].name;
24 | option.value = value;
25 |
26 | // Convert the Token List to an Array
27 | const css = Array.from(target_view_el.classList);
28 |
29 | const value_a = value.split(' ');
30 | const intersection = css.filter(x => value_a.includes(x));
31 |
32 | if(intersection.length === value_a.length) {
33 | option.setAttribute('selected', 'selected');
34 | }
35 |
36 | input.append(option);
37 | }
38 | return input;
39 | },
40 | onUpdate({elInput, component}) {
41 | const classes = component.getClasses();
42 | const opts = this.model.get('options') || [];
43 | for (let i = 0; i < opts.length; i++) {
44 | let value = opts[i].value;
45 | if (value && classes.includes(value)) {
46 | elInput.value = value;
47 | return;
48 | }
49 | }
50 | elInput.value = "GJS_NO_CLASS";
51 | },
52 |
53 | onEvent({elInput, component, event}) {
54 | const classes = this.model.get('options').map(opt => opt.value);
55 | for (let i = 0; i < classes.length; i++) {
56 | if (classes[i].length > 0) {
57 | const classes_i_a = classes[i].split(' ');
58 | for (let j = 0; j < classes_i_a.length; j++) {
59 | if (classes_i_a[j].length > 0) {
60 | component.removeClass(classes_i_a[j]);
61 | }
62 | }
63 | }
64 | }
65 | const value = this.model.get('value');
66 |
67 | // This piece of code removes the empty attribute name from attributes list
68 | const elAttributes = component.attributes.attributes;
69 | delete elAttributes[""];
70 |
71 | if (value.length > 0 && value !== 'GJS_NO_CLASS') {
72 | const value_a = value.split(' ');
73 | for (let i = 0; i < value_a.length; i++) {
74 | component.addClass(value_a[i]);
75 | }
76 | }
77 | component.em.trigger('component:toggled');
78 | },
79 | });
80 |
81 | const textTrait = tm.getType('text');
82 |
83 | tm.addType('content', {
84 | events: {
85 | 'keyup': 'onChange',
86 | },
87 |
88 | onValueChange: function () {
89 | const md = this.model;
90 | const target = md.target;
91 | target.set('content', md.get('value'));
92 | },
93 |
94 | getInputEl: function () {
95 | if (!this.inputEl) {
96 | this.inputEl = textTrait.prototype.getInputEl.bind(this)();
97 | this.inputEl.value = this.target.get('content');
98 | }
99 | return this.inputEl;
100 | }
101 | });
102 |
103 | tm.addType('content', {
104 | events: {
105 | 'keyup': 'onChange',
106 | },
107 |
108 | onValueChange: function () {
109 | const md = this.model;
110 | const target = md.target;
111 | target.set('content', md.get('value'));
112 | },
113 |
114 | getInputEl: function () {
115 | if (!this.inputEl) {
116 | this.inputEl = textTrait.prototype.getInputEl.bind(this)();
117 | this.inputEl.value = this.target.get('content');
118 | }
119 | return this.inputEl;
120 | }
121 | });
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | const elHasClass = (el, toFind) => {
2 | let cls = el.className;
3 | cls = cls && cls.toString();
4 | if (cls && cls.split(' ').indexOf(toFind) >= 0) return 1;
5 | };
6 |
7 | const capitalize = (phrase) => {
8 | return phrase
9 | .toLowerCase()
10 | .split(' ')
11 | .map(word => word.charAt(0).toUpperCase() + word.slice(1))
12 | .join(' ');
13 | };
14 |
15 | export {
16 | elHasClass,
17 | capitalize,
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin');
2 | const TerserPlugin = require("terser-webpack-plugin");
3 | const pkg = require('./package.json');
4 | const webpack = require('webpack');
5 | const fs = require('fs');
6 | const path = require('path')
7 | const name = pkg.name;
8 | let plugins = [];
9 | let optimization = {};
10 |
11 |
12 | module.exports = (env = {}) => {
13 | if (env.production) {
14 | optimization.minimizer = [
15 | new TerserPlugin({
16 | parallel: true,
17 | })
18 | ];
19 | plugins = [
20 | new webpack.BannerPlugin(`${name} - ${pkg.version}`),
21 | ]
22 | } else {
23 | const index = 'index.html';
24 | const indexDev = '_' + index;
25 | plugins.push(new HtmlWebpackPlugin({
26 | template: fs.existsSync(indexDev) ? indexDev : index
27 | }));
28 | }
29 |
30 | return {
31 | mode: env.production ? 'production' : 'development',
32 | entry: './src',
33 | output: {
34 | filename: `./${name}.min.js`,
35 | library: name,
36 | libraryTarget: 'umd',
37 | path: path.resolve(__dirname, 'dist'),
38 | publicPath: '/dist/',
39 | },
40 | devServer: {
41 | static: path.join(__dirname, 'dist'),
42 | hot: true,
43 | },
44 | module: {
45 | rules: [
46 | {
47 | test: /\.js$/,
48 | include: /src/,
49 | use: {
50 | loader: 'babel-loader',
51 | }
52 | },
53 | ],
54 | },
55 | externals: {'grapesjs': 'grapesjs'},
56 | optimization: optimization,
57 | plugins: plugins,
58 | watchOptions: {
59 | ignored: /node_modules/
60 | }
61 | };
62 | };
63 |
--------------------------------------------------------------------------------