├── .gitignore ├── tsconfig.json ├── package.json ├── index.html ├── LICENSE ├── README.md └── src └── index.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | node_modules/ 4 | dist/ 5 | .eslintrc 6 | .npmrc 7 | *.log 8 | _index.html 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/grapesjs-cli/dist/template/tsconfig.json", 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-component-countdown", 3 | "version": "1.0.2", 4 | "description": "Simple countdown component for the GrapesJS Editor", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist/" 8 | ], 9 | "scripts": { 10 | "build": "grapesjs-cli build", 11 | "start": "grapesjs-cli serve" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/GrapesJS/components-countdown.git" 16 | }, 17 | "keywords": [ 18 | "grapesjs", 19 | "plugin", 20 | "component", 21 | "countdown", 22 | "wysiwyg" 23 | ], 24 | "author": "Artur Arseniev", 25 | "license": "BSD-3-Clause", 26 | "devDependencies": { 27 | "grapesjs": "^0.21.2", 28 | "grapesjs-cli": "^4.1.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS Countdown Plugin 6 | 7 | 8 | 14 | 15 | 16 |
17 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Artur Arseniev 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 Countdown 2 | 3 | Simple countdown component for GrapesJS Editor 4 | 5 |

GrapesJS

6 |
7 | 8 | # [Demo](http://grapesjs.com/demo.html) 9 | 10 | 11 | 12 | 13 | 14 | ## Summary 15 | 16 | * Plugin name: `grapesjs-component-countdown` 17 | * Components: `countdown` 18 | * Blocks: `countdown` 19 | 20 | 21 | ## Options 22 | 23 | | Option | Description | Default | 24 | |-|-|- 25 | | `id` | The ID used to create the block and component. | `countdown` | 26 | | `label` | The label used for the block and the component. | `Countdown` | 27 | | `block` | Object to extend the default block, eg. `{ label: 'Countdown', category: 'Extra', ... }`. Pass a falsy value to avoid adding the block. | `{}` | 28 | | `props` | Object to extend the default component properties., eg. `{ name: 'Countdown', droppable: false, ... }`. | `{}` | 29 | | `style` | Custom CSS styles for the component. This will replace the default one. | `''` | 30 | | `styleAdditional` | Additional CSS styles for the component. These will be appended to the default one. | `''` | 31 | | `startTime` | Default start time, eg. `2030-01-25 00:00`. | `''` | 32 | | `endText` | Text to show when the countdown is ended. | `'EXPIRED'` | 33 | | `dateInputType` | Date input type, eg. `date`, `datetime-local` | `'date'` | 34 | | `labelDays` | Days label text used in component. | `'days'` | 35 | | `labelHours` | Hours label text used in component. | `'hours'` | 36 | | `labelMinutes` | Minutes label text used in component. | `'minutes'` | 37 | | `labelSeconds` | Seconds label text used in component. | `'seconds'` | 38 | | `classPrefix` | Countdown component class prefix. | `'countdown'` | 39 | 40 | 41 | 42 | 43 | 44 | ## Download 45 | 46 | * CDN 47 | * `https://unpkg.com/grapesjs-component-countdown` 48 | * NPM 49 | * `npm i grapesjs-component-countdown` 50 | * GIT 51 | * `git clone https://github.com/artf/grapesjs-component-countdown.git` 52 | 53 | 54 | 55 | 56 | 57 | 58 | ## Usage 59 | 60 | Directly in the browser 61 | ```html 62 | 63 | 64 | 65 | 66 |
67 | 68 | 78 | ``` 79 | 80 | Modern javascript 81 | ```js 82 | import grapesjs from 'grapesjs'; 83 | import pluginCountdown from 'grapesjs-component-countdown'; 84 | 85 | const editor = grapesjs.init({ 86 | container : '#gjs', 87 | // ... 88 | plugins: [pluginCountdown], 89 | pluginsOpts: { 90 | [pluginCountdown]: { /* options */ } 91 | } 92 | // or 93 | plugins: [ 94 | editor => pluginCountdown(editor, { /* options */ }), 95 | ], 96 | }); 97 | ``` 98 | 99 | 100 | 101 | 102 | 103 | ## Development 104 | 105 | Clone the repository 106 | 107 | ```sh 108 | $ git clone https://github.com/artf/grapesjs-component-countdown.git 109 | $ cd grapesjs-component-countdown 110 | ``` 111 | 112 | Install it 113 | 114 | ```sh 115 | $ npm i 116 | ``` 117 | 118 | Start the dev server 119 | 120 | ```sh 121 | $ npm start 122 | ``` 123 | 124 | Build before the commit. This will also increase the patch level version of the package 125 | 126 | ```sh 127 | $ npm run build 128 | ``` 129 | 130 | 131 | 132 | 133 | 134 | ## License 135 | 136 | BSD 3-Clause 137 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, BlockProperties, ComponentDefinition } from 'grapesjs'; 2 | 3 | export type PluginOptions = { 4 | /** 5 | * The ID used to create the block and component 6 | * @default 'countdown' 7 | */ 8 | id?: string; 9 | 10 | /** 11 | * The label used for the block and the component. 12 | * @default 'Countdown' 13 | */ 14 | label?: string, 15 | 16 | /** 17 | * Object to extend the default block. Pass a falsy value to avoid adding the block. 18 | * @example 19 | * { label: 'Countdown', category: 'Extra', ... } 20 | */ 21 | block?: Partial; 22 | 23 | /** 24 | * Object to extend the default component properties. 25 | * @example 26 | * { name: 'Countdown', droppable: false, ... } 27 | */ 28 | props?: ComponentDefinition; 29 | 30 | /** 31 | * Custom CSS styles for the component. This will replace the default one. 32 | * @default '' 33 | */ 34 | style?: string, 35 | 36 | /** 37 | * Additional CSS styles for the component. These will be appended to the default one. 38 | * @default '' 39 | */ 40 | styleAdditional?: string, 41 | 42 | /** 43 | * Default start time. 44 | * @default '' 45 | * @example '2018-01-25 00:00' 46 | */ 47 | startTime?: string, 48 | 49 | /** 50 | * Text to show when the countdown is ended. 51 | * @default 'EXPIRED' 52 | */ 53 | endText?: string, 54 | 55 | /** 56 | * Date input type, eg. `date`, `datetime-local` 57 | * @default 'date' 58 | */ 59 | dateInputType?: string, 60 | 61 | /** 62 | * Days label text used in component. 63 | * @default 'days' 64 | */ 65 | labelDays?: string, 66 | 67 | /** 68 | * Hours label text used in component. 69 | * @default 'hours' 70 | */ 71 | labelHours?: string, 72 | 73 | /** 74 | * Minutes label text used in component. 75 | * @default 'minutes' 76 | */ 77 | labelMinutes?: string, 78 | 79 | /** 80 | * Seconds label text used in component. 81 | * @default 'seconds' 82 | */ 83 | labelSeconds?: string, 84 | 85 | /** 86 | * Countdown component class prefix. 87 | * @default 'countdown' 88 | */ 89 | classPrefix?: string, 90 | }; 91 | 92 | type TElement = HTMLElement & { __gjsCountdownInterval: NodeJS.Timer }; 93 | 94 | declare global { 95 | interface Window { __gjsCountdownIntervals: TElement[]; } 96 | } 97 | 98 | const plugin: Plugin = (editor, opts = {}) => { 99 | const options: PluginOptions = { 100 | id: 'countdown', 101 | label: 'Countdown', 102 | block: {}, 103 | props: {}, 104 | style: '', 105 | styleAdditional: '', 106 | startTime: '', 107 | endText: 'EXPIRED', 108 | dateInputType: 'date', 109 | labelDays: 'days', 110 | labelHours: 'hours', 111 | labelMinutes: 'minutes', 112 | labelSeconds: 'seconds', 113 | classPrefix: 'countdown', 114 | ...opts, 115 | }; 116 | 117 | const { block, props, style } = options; 118 | const id = options.id!; 119 | const label = options.label!; 120 | const pfx = options.classPrefix!; 121 | 122 | // Create block 123 | if (block) { 124 | editor.Blocks.add(id, { 125 | media: ` 126 | 127 | `, 128 | label, 129 | category: 'Extra', 130 | select: true, 131 | content: { type: id }, 132 | ...block 133 | }); 134 | }; 135 | 136 | const coundownScript = function(props: Record) { 137 | const startfrom: string = props.startfrom; 138 | const endTxt: string = props.endText; 139 | // @ts-ignore 140 | const el: TElement = this; 141 | const countDownDate = new Date(startfrom).getTime(); 142 | const countdownEl = el.querySelector('[data-js=countdown]') as HTMLElement; 143 | const endTextEl = el.querySelector('[data-js=countdown-endtext]') as HTMLElement; 144 | const dayEl = el.querySelector('[data-js=countdown-day]')!; 145 | const hourEl = el.querySelector('[data-js=countdown-hour]')!; 146 | const minuteEl = el.querySelector('[data-js=countdown-minute]')!; 147 | const secondEl = el.querySelector('[data-js=countdown-second]')!; 148 | const oldInterval = el.__gjsCountdownInterval; 149 | oldInterval && clearInterval(oldInterval); 150 | 151 | const connected: TElement[] = window.__gjsCountdownIntervals || []; 152 | const toClean: TElement[] = []; 153 | connected.forEach((item: TElement) => { 154 | if (!item.isConnected) { 155 | clearInterval(item.__gjsCountdownInterval); 156 | toClean.push(item); 157 | } 158 | }); 159 | connected.indexOf(el) < 0 && connected.push(el); 160 | window.__gjsCountdownIntervals = connected.filter(item => toClean.indexOf(item) < 0); 161 | 162 | const setTimer = function (days: number, hours: number, minutes: number, seconds: number) { 163 | dayEl.innerHTML = `${days < 10 ? '0' + days : days}`; 164 | hourEl.innerHTML = `${hours < 10 ? '0' + hours : hours}`; 165 | minuteEl.innerHTML = `${minutes < 10 ? '0' + minutes : minutes}`; 166 | secondEl.innerHTML = `${seconds < 10 ? '0' + seconds : seconds}`; 167 | } 168 | 169 | const moveTimer = function() { 170 | const now = new Date().getTime(); 171 | const distance = countDownDate - now; 172 | const days = Math.floor(distance / 86400000); 173 | const hours = Math.floor((distance % 86400000) / 3600000); 174 | const minutes = Math.floor((distance % 3600000) / 60000); 175 | const seconds = Math.floor((distance % 60000) / 1000); 176 | 177 | setTimer(days, hours, minutes, seconds); 178 | 179 | if (distance < 0) { 180 | clearInterval(el.__gjsCountdownInterval); 181 | endTextEl.innerHTML = endTxt; 182 | countdownEl.style.display = 'none'; 183 | endTextEl.style.display = ''; 184 | } 185 | }; 186 | 187 | if (countDownDate) { 188 | el.__gjsCountdownInterval = setInterval(moveTimer, 1000); 189 | endTextEl.style.display = 'none'; 190 | countdownEl.style.display = ''; 191 | moveTimer(); 192 | } else { 193 | setTimer(0, 0, 0, 0); 194 | } 195 | }; 196 | 197 | // Create component 198 | editor.Components.addType(id, { 199 | model: { 200 | defaults: { 201 | startfrom: options.startTime, 202 | classes: [pfx], 203 | endText: options.endText, 204 | droppable: false, 205 | script: coundownScript, 206 | 'script-props': ['startfrom', 'endText'], 207 | traits: [{ 208 | label: 'Start', 209 | name: 'startfrom', 210 | changeProp: true, 211 | type: options.dateInputType, 212 | },{ 213 | label: 'End text', 214 | name: 'endText', 215 | changeProp: true, 216 | }], 217 | // @ts-ignore 218 | components: ` 219 | 220 |
221 |
222 |
${options.labelDays}
223 |
224 |
225 |
226 |
${options.labelHours}
227 |
228 |
229 |
230 |
${options.labelMinutes}
231 |
232 |
233 |
234 |
${options.labelSeconds}
235 |
236 |
237 | 238 | `, 239 | styles: (style || ` 240 | .${pfx} { 241 | text-align: center; 242 | } 243 | 244 | .${pfx}-block { 245 | display: inline-block; 246 | margin: 0 10px; 247 | padding: 10px; 248 | } 249 | 250 | .${pfx}-digit { 251 | font-size: 5rem; 252 | } 253 | 254 | .${pfx}-endtext { 255 | font-size: 5rem; 256 | } 257 | 258 | .${pfx}-cont { 259 | display: inline-block; 260 | } 261 | `) + (options.styleAdditional), 262 | ...props, 263 | }, 264 | }, 265 | }); 266 | }; 267 | 268 | export default plugin; --------------------------------------------------------------------------------