├── .env ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── assets └── css │ └── demo.css ├── deno.json ├── mod.ts ├── plugins.json ├── scripts └── buildNpm.ts └── site ├── components ├── BaseLayout.json ├── Link.json ├── MainFooter.json ├── MainNavigation.json ├── Markdown.json ├── MetaFields.json ├── Navigation.json └── Scripts.json ├── dataSources.ts ├── layouts └── siteIndex.json ├── meta.json ├── pageUtilities.ts ├── routes.json ├── scripts └── demo.ts ├── transforms └── markdown.ts └── twindSetup.ts /.env: -------------------------------------------------------------------------------- 1 | GUSTWIND_VERSION=0.39.12 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: gh-pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - "*" 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Setup deno 17 | uses: denolib/setup-deno@v2 18 | with: 19 | deno-version: v1.37.2 20 | 21 | - name: Build gh-pages 22 | run: | 23 | deno --version 24 | deno task build 25 | 26 | - name: Deploy gh-pages 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: ./public 31 | 32 | - name: Get tag version 33 | if: startsWith(github.ref, 'refs/tags/') 34 | id: get_tag_version 35 | run: echo ::set-output name=TAG_VERSION::${GITHUB_REF/refs\/tags\//} 36 | 37 | - uses: actions/setup-node@v2 38 | with: 39 | node-version: "18.x" 40 | registry-url: "https://registry.npmjs.org" 41 | 42 | - name: npm build 43 | run: deno run -A ./scripts/buildNpm.ts ${{steps.get_tag_version.outputs.TAG_VERSION}} 44 | 45 | - name: npm publish 46 | if: startsWith(github.ref, 'refs/tags/') 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | run: | 50 | cd npm 51 | npm publish 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm 2 | public 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.unstable": true, 4 | "deno.lint": true, 5 | "[json]": { 6 | "editor.defaultFormatter": "denoland.vscode-deno", 7 | }, 8 | "[md]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode", 10 | }, 11 | "[typescript]": { 12 | "editor.defaultFormatter": "denoland.vscode-deno", 13 | }, 14 | "[typescriptreact]": { 15 | "editor.defaultFormatter": "denoland.vscode-deno", 16 | }, 17 | "deno.import_intellisense_origins": { 18 | "https://deno.land": true 19 | }, 20 | "deno.suggest.imports.hosts": { 21 | "https://unpkg.com": false, 22 | "https://esm.sh": false, 23 | "https://deno.land": false 24 | } 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Juho Vepsäläinen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **dragjs** has been designed to make it easy to create JavaScript based drag interactions. This includes use cases such as draggable panels and different types of sliders. The idea is that you use the logic from this package to build your own components as it captures concerns such as mouse and touch handling. 2 | 3 | ## Demonstrations 4 | 5 |
6 | 7 | ### Simple draggable 8 | 9 |
10 |
Drag me!
11 |
12 | 13 | 14 | ```typescript 15 | import { draggable } from "dragjs"; 16 | 17 | const draggableElement = document.getElementById("draggable"); 18 | 19 | draggableElement && draggable({ element: draggableElement }); 20 | ``` 21 | 22 | ### Draggable with a specific handle 23 | 24 |
25 |
26 |
Header
27 |
Body
28 |
29 |
30 | 31 | 32 | ```typescript 33 | import { draggable } from "dragjs"; 34 | 35 | const element = document.getElementById("draggabletwo"); 36 | const handle = element.children[0]; 37 | 38 | element && handle && draggable({ element, handle }); 39 | ``` 40 | 41 | ### 1D slider 42 | 43 |
44 | 45 | ```typescript 46 | import { slider } from "dragjs"; 47 | 48 | const onedContainer = document.getElementById("onedContainer"); 49 | 50 | onedContainer && slider({ 51 | parent: onedContainer, 52 | "class": "oned", 53 | cbs: { 54 | begin: () => { 55 | log("2dslider: begin"); 56 | }, 57 | change: ({ x, pointer }) => { 58 | const newX = clamp(x * 100, 0, 100).toFixed(2) + "%"; 59 | 60 | console.log("2dslider: " + newX); 61 | 62 | if (pointer) { 63 | pointer.style.left = newX; 64 | } 65 | }, 66 | end: () => { 67 | log("2dslider: end"); 68 | }, 69 | }, 70 | }); 71 | ``` 72 | 73 | ### 2D slider 74 | 75 |
76 | 77 | ```typescript 78 | import { xyslider } from "dragjs"; 79 | 80 | const twodContainer = document.getElementById("twodContainer"); 81 | 82 | twodContainer && xyslider({ 83 | parent: twodContainer, 84 | "class": "twod", 85 | cbs: { 86 | change: ({ x, y, pointer }) => { 87 | const newX = clamp(x * 100, 0, 100).toFixed(2) + "%"; 88 | const newY = clamp(y * 100, 0, 100).toFixed(2) + "%"; 89 | 90 | console.log("x: " + newX + ", y: " + newY); 91 | 92 | if (pointer) { 93 | pointer.style.left = newX; 94 | pointer.style.top = newY; 95 | } 96 | }, 97 | }, 98 | }); 99 | ``` 100 | 101 | ## Contributors 102 | 103 | * [Jean Carrière](https://github.com/JeanCarriere) 104 | * [Sam Potts](https://github.com/SamPotts) - Use getBoundingClientRect(), IE9+ support 105 | 106 | ## Development 107 | 108 | Run the available commands through `deno task`. 109 | 110 | To publish, tag a release with the desired version (i.e. `git tag 0.13.0`) and then `git push`. 111 | 112 | ## License 113 | 114 | **dragjs** is available under MIT. See LICENSE for more details. 115 | -------------------------------------------------------------------------------- /assets/css/demo.css: -------------------------------------------------------------------------------- 1 | #value { 2 | position: fixed; 3 | right: 0; 4 | bottom: 0; 5 | margin-right: 1em; 6 | margin-bottom: 1em; 7 | } 8 | 9 | #draggableParent { 10 | position: relative; 11 | } 12 | 13 | #draggable { 14 | right: 0; 15 | position: absolute; 16 | background-color: #ada; 17 | border: 1px solid black; 18 | width: 100px; 19 | height: 100px; 20 | text-align: center; 21 | cursor: move; 22 | } 23 | 24 | #draggabletwo { 25 | right: 0; 26 | position: absolute; 27 | background-color: #ada; 28 | border: 1px solid black; 29 | width: 100px; 30 | height: 100px; 31 | text-align: center; 32 | cursor: move; 33 | } 34 | 35 | #twodContainer { 36 | top: 50px; 37 | left: 50px; 38 | overflow: scroll; 39 | width: 150px; 40 | height: 150px; 41 | } 42 | 43 | .twod { 44 | background-color: #aad; 45 | border: 1px solid black; 46 | width: 200px; 47 | height: 200px; 48 | cursor: move; 49 | } 50 | 51 | .twod .pointer { 52 | position: relative; 53 | z-index: 2; 54 | margin-left: -7px; 55 | margin-top: -7px; 56 | width: 10px; 57 | height: 10px; 58 | border: 2px solid black; 59 | -mox-border-radius: 5px; 60 | border-radius: 5px; 61 | } 62 | 63 | .oned { 64 | background-color: #daa; 65 | border: 1px solid black; 66 | width: 300px; 67 | height: 30px; 68 | cursor: move; 69 | } 70 | 71 | .oned .pointer { 72 | position: relative; 73 | background-color: black; 74 | z-index: 2; 75 | margin-left: -2px; 76 | width: 4px; 77 | height: 30px; 78 | } 79 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": "deno run -A --unstable --no-check https://deno.land/x/gustwind@v0.39.12/gustwind-cli/mod.ts -b -t cpuHalf -o ./public", 4 | "build:linked": "gustwind -b -t cpuHalf -o ./public", 5 | "start": "deno run -A --unstable --no-check https://deno.land/x/gustwind@v0.39.12/gustwind-cli/mod.ts -d -p 3000", 6 | "start:linked": "gustwind -d -p 3000", 7 | "serve": "deno run -A --unstable --no-check https://deno.land/x/gustwind@v0.39.12/gustwind-cli/mod.ts -s -p 3000 -i ./public", 8 | "serve:linked": "gustwind -s -p 3000 -i ./public" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | type Callback = ( 4 | { x, y, cursor, elem, pointer }: { 5 | x: number; 6 | y: number; 7 | cursor: Coordinate; 8 | elem: HTMLElement; 9 | e: MouseEvent | TouchEvent; 10 | pointer?: HTMLElement; 11 | }, 12 | ) => boolean | void; 13 | type Callbacks = { 14 | begin?: Callback; 15 | change?: Callback; 16 | end?: Callback; 17 | }; 18 | type Position = "left" | "right"; 19 | 20 | function draggable( 21 | { element, handle, xPosition }: { 22 | element: HTMLElement; 23 | handle?: HTMLElement; 24 | xPosition?: Position; 25 | }, 26 | cbs?: Callbacks, 27 | ) { 28 | if (!element) { 29 | console.warn("drag is missing elem!"); 30 | return; 31 | } 32 | 33 | dragTemplate( 34 | element, 35 | "touchstart", 36 | "touchmove", 37 | "touchend", 38 | cbs, 39 | handle, 40 | xPosition, 41 | ); 42 | dragTemplate( 43 | element, 44 | "mousedown", 45 | "mousemove", 46 | "mouseup", 47 | cbs, 48 | handle, 49 | xPosition, 50 | ); 51 | } 52 | 53 | function xyslider(o: { parent: HTMLElement; class: string; cbs: Callbacks }) { 54 | const twod = div(o["class"] || "", o.parent); 55 | const pointer = div("pointer", twod); 56 | 57 | div("shape shape1", pointer); 58 | div("shape shape2", pointer); 59 | div("bg bg1", twod); 60 | div("bg bg2", twod); 61 | 62 | draggable({ element: twod }, attachPointer(o.cbs, pointer)); 63 | 64 | return { 65 | background: twod, 66 | pointer, 67 | }; 68 | } 69 | 70 | function slider(o: { parent: HTMLElement; class: string; cbs: Callbacks }) { 71 | const oned = div(o["class"], o.parent); 72 | const pointer = div("pointer", oned); 73 | 74 | div("shape", pointer); 75 | div("bg", oned); 76 | 77 | draggable({ element: oned }, attachPointer(o.cbs, pointer)); 78 | 79 | return { 80 | background: oned, 81 | pointer: pointer, 82 | }; 83 | } 84 | 85 | function attachPointer(cbs: Callbacks, pointer: HTMLElement): Callbacks { 86 | const ret: Record = {}; 87 | 88 | Object.entries(cbs).forEach(([name, callback]) => { 89 | ret[name] = (p) => { 90 | callback({ ...p, pointer }); 91 | }; 92 | }); 93 | 94 | return ret as Callbacks; 95 | } 96 | 97 | // move to elemutils lib? 98 | function div(klass: string, p: HTMLElement) { 99 | return e("div", klass, p); 100 | } 101 | 102 | function e(type: string, klass: string, p: HTMLElement) { 103 | const elem = document.createElement(type); 104 | 105 | if (klass) { 106 | elem.className = klass; 107 | } 108 | p.appendChild(elem); 109 | 110 | return elem; 111 | } 112 | 113 | function dragTemplate( 114 | elem: HTMLElement, 115 | down: string, 116 | move: string, 117 | up: string, 118 | cbs?: Callbacks, 119 | handle?: HTMLElement, 120 | xPosition?: Position, 121 | ) { 122 | cbs = getCbs(cbs, xPosition); 123 | 124 | const beginCb = cbs.begin; 125 | const changeCb = cbs.change; 126 | const endCb = cbs.end; 127 | 128 | on(handle || elem, down, (e) => { 129 | // @ts-ignore Figure out how to type this 130 | const moveHandler = (e: Event) => callCb(changeCb, elem, e); 131 | 132 | function upHandler() { 133 | off(document, move, moveHandler); 134 | off(document, up, upHandler); 135 | 136 | // @ts-ignore Figure out how to type this 137 | callCb(endCb, elem, e); 138 | } 139 | 140 | on(document, move, moveHandler); 141 | on(document, up, upHandler); 142 | 143 | // @ts-ignore Figure out how to type this 144 | callCb(beginCb, elem, e); 145 | }); 146 | } 147 | 148 | // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection 149 | function on( 150 | elem: HTMLElement | Document, 151 | evt: string, 152 | handler: (e: Event) => void, 153 | ) { 154 | // Test via a getter in the options object to see if the passive property is accessed 155 | let supportsPassive = false; 156 | try { 157 | const opts = Object.defineProperty({}, "passive", { 158 | get: function () { 159 | supportsPassive = true; 160 | 161 | return undefined; 162 | }, 163 | }); 164 | 165 | // @ts-ignore Deno doesn't know this 166 | // deno-shim-ignore 167 | globalThis.addEventListener("testPassive", null, opts); 168 | 169 | // @ts-ignore Deno doesn't know this 170 | // deno-shim-ignore 171 | globalThis.removeEventListener("testPassive", null, opts); 172 | } catch (err) { 173 | console.error(err); 174 | } 175 | 176 | elem.addEventListener( 177 | evt, 178 | handler, 179 | supportsPassive ? { passive: false } : false, 180 | ); 181 | } 182 | 183 | function off( 184 | elem: HTMLElement | Document, 185 | evt: string, 186 | handler: (event: Event) => void, 187 | ) { 188 | elem.removeEventListener(evt, handler, false); 189 | } 190 | 191 | type Coordinate = { x: number; y: number }; 192 | 193 | function getCbs( 194 | cbs?: Callbacks, 195 | xPosition: Position = "left", 196 | ): Callbacks { 197 | let initialOffset: Coordinate; 198 | let initialPos: Coordinate; 199 | 200 | const defaultCbs: Callbacks = { 201 | begin: (c) => { 202 | const bodyWidth = document.body.clientWidth; 203 | 204 | initialOffset = { 205 | x: xPosition === "left" 206 | ? c.elem.offsetLeft 207 | : bodyWidth - c.elem.offsetLeft - c.elem.clientWidth, 208 | y: c.elem.offsetTop, 209 | }; 210 | initialPos = xPosition === "left" 211 | ? c.cursor 212 | : { x: bodyWidth - c.cursor.x, y: c.cursor.y }; 213 | }, 214 | change: (c) => { 215 | if ( 216 | typeof initialOffset.x !== "number" || typeof c.cursor.x !== "number" || 217 | typeof initialPos.x !== "number" 218 | ) { 219 | return; 220 | } 221 | 222 | const bodyWidth = document.body.clientWidth; 223 | 224 | style( 225 | c.elem, 226 | xPosition, 227 | xPosition === "left" 228 | ? (initialOffset.x + c.cursor.x - initialPos.x) + "px" 229 | : (initialOffset.x + (bodyWidth - c.cursor.x) - initialPos.x) + "px", 230 | ); 231 | 232 | if ( 233 | typeof initialOffset.y !== "number" || typeof c.cursor.y !== "number" || 234 | typeof initialPos.y !== "number" 235 | ) { 236 | return; 237 | } 238 | 239 | style( 240 | c.elem, 241 | "top", 242 | (initialOffset.y + c.cursor.y - initialPos.y) + "px", 243 | ); 244 | }, 245 | end: () => {}, 246 | }; 247 | 248 | if (cbs) { 249 | return { 250 | begin: cbs.begin 251 | ? ((args) => { 252 | const ret = cbs.begin && cbs.begin(args); 253 | 254 | if (ret) { 255 | // @ts-ignore This is fine 256 | return defaultCbs.begin(args); 257 | } 258 | }) 259 | : defaultCbs.begin, 260 | change: cbs.change 261 | ? ((args) => { 262 | const ret = cbs.change && cbs.change(args); 263 | 264 | if (ret) { 265 | // @ts-ignore This is fine 266 | return defaultCbs.change(args); 267 | } 268 | }) 269 | : defaultCbs.change, 270 | end: cbs.end || defaultCbs.end, 271 | }; 272 | } 273 | 274 | return defaultCbs; 275 | } 276 | 277 | function style(e: HTMLElement, prop: string, value: string) { 278 | // @ts-ignore https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style#setting_styles 279 | e.style[prop] = value; 280 | } 281 | 282 | function callCb( 283 | cb: ( 284 | { x, y, cursor, elem, e }: { 285 | x: number; 286 | y: number; 287 | cursor: Coordinate; 288 | elem: HTMLElement; 289 | e: Event; 290 | }, 291 | ) => void, 292 | elem: HTMLElement, 293 | e: MouseEvent | TouchEvent, 294 | ) { 295 | e.preventDefault(); 296 | 297 | // http://www.quirksmode.org/js/findpos.html 298 | const offset = elem.getBoundingClientRect(); 299 | const width = elem.clientWidth; 300 | const height = elem.clientHeight; 301 | const cursor = { 302 | x: cursorX(elem, e), 303 | y: cursorY(elem, e), 304 | }; 305 | 306 | if (typeof cursor.x !== "number" || typeof cursor.y !== "number") { 307 | return; 308 | } 309 | 310 | const x = (cursor.x - offset.left) / width; 311 | const y = (cursor.y - offset.top) / height; 312 | 313 | cb({ 314 | x: isNaN(x) ? 0 : x, 315 | y: isNaN(y) ? 0 : y, 316 | cursor: cursor as Coordinate, 317 | elem, 318 | e, 319 | }); 320 | } 321 | 322 | // http://javascript.about.com/library/blmousepos.htm 323 | function cursorX(_elem: HTMLElement, evt: MouseEvent | TouchEvent) { 324 | // https://github.com/bebraw/dragjs/issues/8 325 | if (window.TouchEvent && evt instanceof window.TouchEvent) { 326 | return evt.touches.item(0)?.clientX; 327 | } 328 | 329 | return (evt as MouseEvent).clientX; 330 | } 331 | function cursorY(_elem: HTMLElement, evt: MouseEvent | TouchEvent) { 332 | // https://github.com/bebraw/dragjs/issues/8 333 | if (window.TouchEvent && evt instanceof window.TouchEvent) { 334 | return evt.touches.item(0)?.clientY; 335 | } 336 | 337 | return (evt as MouseEvent).clientY; 338 | } 339 | 340 | export { draggable, slider, xyslider }; 341 | -------------------------------------------------------------------------------- /plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "https://deno.land/x/gustwind@v${GUSTWIND_VERSION}/plugins/breezewind-router/mod.ts", 4 | "options": { 5 | "dataSourcesPath": "./site/dataSources.ts", 6 | "routesPath": "./site/routes.json" 7 | } 8 | }, 9 | { 10 | "path": "https://deno.land/x/gustwind@v${GUSTWIND_VERSION}/plugins/breezewind-renderer/mod.ts", 11 | "options": { 12 | "componentsPath": "./site/components", 13 | "metaPath": "./site/meta.json", 14 | "layoutsPath": "./site/layouts", 15 | "pageUtilitiesPath": "./site/pageUtilities.ts" 16 | } 17 | }, 18 | { 19 | "path": "https://deno.land/x/gustwind@v${GUSTWIND_VERSION}/plugins/copy/mod.ts", 20 | "options": { 21 | "inputPath": "./assets", 22 | "outputPath": "./assets" 23 | } 24 | }, 25 | { 26 | "path": "https://deno.land/x/gustwind@v${GUSTWIND_VERSION}/plugins/script/mod.ts", 27 | "options": { 28 | "scripts": [], 29 | "scriptsPath": ["./site/scripts"] 30 | } 31 | }, 32 | { 33 | "path": "https://deno.land/x/gustwind@v${GUSTWIND_VERSION}/plugins/twind/mod.ts", 34 | "options": { "setupPath": "./site/twindSetup.ts" } 35 | } 36 | ] 37 | -------------------------------------------------------------------------------- /scripts/buildNpm.ts: -------------------------------------------------------------------------------- 1 | // ex. scripts/build_npm.ts 2 | import { build } from "https://deno.land/x/dnt@0.7.0/mod.ts"; 3 | 4 | await build({ 5 | entryPoints: ["./mod.ts"], 6 | outDir: "./npm", 7 | package: { 8 | // package.json properties 9 | name: "dragjs", 10 | version: Deno.args[0], 11 | description: 12 | "Simple utility to make it easier to implement drag based things (ie. sliders and such)", 13 | license: "MIT", 14 | repository: { 15 | type: "git", 16 | url: "git+https://github.com/bebraw/dragjs.git", 17 | }, 18 | bugs: { 19 | url: "https://github.com/bebraw/dragjs/issues", 20 | }, 21 | keywords: ["drag", "dragging", "draggable"], 22 | }, 23 | }); 24 | 25 | // post build steps 26 | Deno.copyFileSync("LICENSE", "npm/LICENSE"); 27 | Deno.copyFileSync("README.md", "npm/README.md"); 28 | -------------------------------------------------------------------------------- /site/components/BaseLayout.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "!DOCTYPE", 4 | "attributes": { 5 | "html": "" 6 | }, 7 | "closingCharacter": "" 8 | }, 9 | { 10 | "type": "html", 11 | "attributes": { 12 | "lang": { 13 | "utility": "get", 14 | "parameters": ["context", "language"] 15 | } 16 | }, 17 | "children": [ 18 | { 19 | "type": "head", 20 | "children": [ 21 | { 22 | "type": "MetaFields" 23 | } 24 | ] 25 | }, 26 | { 27 | "type": "body", 28 | "children": [ 29 | { 30 | "type": "MainNavigation" 31 | }, 32 | { 33 | "visibleIf": [{ 34 | "utility": "get", 35 | "parameters": ["props", "showToc"] 36 | }], 37 | "type": "aside", 38 | "class": "fixed top-16 pl-4 hidden lg:inline", 39 | "children": [ 40 | { 41 | "type": "TableOfContents" 42 | } 43 | ] 44 | }, 45 | { 46 | "type": "main", 47 | "children": { 48 | "utility": "render", 49 | "parameters": [ 50 | { 51 | "utility": "get", 52 | "parameters": ["props", "content"] 53 | } 54 | ] 55 | } 56 | }, 57 | { 58 | "type": "MainFooter" 59 | }, 60 | { 61 | "type": "Scripts" 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /site/components/Link.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "a", 3 | "class": "underline", 4 | "classList": { 5 | "font-bold": [ 6 | { "utility": "get", "parameters": ["props", "href"] }, 7 | { 8 | "utility": "trim", 9 | "parameters": [ 10 | { "utility": "get", "parameters": ["context", "pagePath"] }, 11 | "/" 12 | ] 13 | } 14 | ] 15 | }, 16 | "children": { 17 | "utility": "render", 18 | "parameters": [ 19 | { 20 | "utility": "get", 21 | "parameters": ["props", "children"] 22 | } 23 | ] 24 | }, 25 | "attributes": { 26 | "href": { 27 | "utility": "validateUrl", 28 | "parameters": [ 29 | { 30 | "utility": "get", 31 | "parameters": ["props", "href"] 32 | } 33 | ] 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /site/components/MainFooter.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "footer", 3 | "class": "py-8 font-light bg-gradient-to-br from-green-100 to-blue-200", 4 | "children": [ 5 | { 6 | "type": "div", 7 | "class": "flex flex-col container mx-auto gap-4", 8 | "children": [ 9 | { 10 | "type": "section", 11 | "class": "md:mx-auto px-4 sm:px-0 grid grid-rows-3 md:grid-rows-none md:grid-cols-3 gap-4 md:gap-8 w-full md:max-w-3xl", 12 | "children": [ 13 | { 14 | "type": "div", 15 | "class": "flex flex-col", 16 | "children": [ 17 | { 18 | "type": "h2", 19 | "children": "About" 20 | }, 21 | { 22 | "type": "div", 23 | "class": "font-thin", 24 | "children": "dragjs makes it easy to implement draggables in JavaScript." 25 | } 26 | ] 27 | }, 28 | { 29 | "type": "div", 30 | "class": "flex flex-col", 31 | "children": [ 32 | { 33 | "type": "h2", 34 | "children": "Navigation" 35 | }, 36 | { 37 | "type": "div", 38 | "class": "flex flex-col gap-2 font-thin", 39 | "children": [ 40 | { 41 | "type": "Navigation" 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | { 48 | "type": "div", 49 | "class": "flex flex-col", 50 | "children": [ 51 | { 52 | "type": "h2", 53 | "children": "Credits" 54 | }, 55 | { 56 | "type": "Markdown", 57 | "props": { 58 | "type": "div", 59 | "class": "font-thin", 60 | "inputText": "dragjs was developed by [Juho Vepsäläinen](https://github.com/bebraw)." 61 | } 62 | } 63 | ] 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | ] 70 | } 71 | 72 | -------------------------------------------------------------------------------- /site/components/MainNavigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "nav", 3 | "class": "sticky top-0 opacity-80 px-8 py-4 gap-4 flex flex-col sm:flex-row justify-between bg-gray-100 font-thin filter drop-shadow-sm", 4 | "children": [ 5 | { 6 | "type": "div", 7 | "class": "flex flex-wrap gap-4 ml-auto sm:ml-0", 8 | "children": [ 9 | { 10 | "type": "a", 11 | "class": "scale-150 whitespace-nowrap", 12 | "children": "✥", 13 | "attributes": { 14 | "href": "/" 15 | } 16 | }, 17 | { 18 | "type": "Navigation" 19 | } 20 | ] 21 | }, 22 | { 23 | "type": "div", 24 | "class": "flex gap-4 ml-auto sm:ml-0", 25 | "children": [ 26 | { 27 | "type": "Link", 28 | "props": { 29 | "children": "GitHub", 30 | "href": "https://github.com/bebraw/dragjs" 31 | } 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | 38 | -------------------------------------------------------------------------------- /site/components/Markdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": { "utility": "get", "parameters": ["props", "type"] }, 3 | "class": { "utility": "get", "parameters": ["props", "class"] }, 4 | "children": { 5 | "utility": "processMarkdown", 6 | "parameters": [{ "utility": "get", "parameters": ["props", "inputText"] }] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /site/components/MetaFields.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "link", 4 | "attributes": { 5 | "rel": "icon", 6 | "href": "data:image/svg+xml," 7 | } 8 | }, 9 | { 10 | "type": "link", 11 | "attributes": { 12 | "rel": "stylesheet", 13 | "href": "./assets/css/demo.css" 14 | } 15 | }, 16 | { 17 | "_reference": "https://web.dev/defer-non-critical-css/", 18 | "type": "link", 19 | "attributes": { 20 | "rel": "preload", 21 | "href": "https://cdn.jsdelivr.net/gh/highlightjs/highlight.js@11.3.1/src/styles/github-dark.min.css", 22 | "as": "style", 23 | "onload": "this.onload=null;this.rel='stylesheet'" 24 | } 25 | }, 26 | { 27 | "type": "noscript", 28 | "children": [ 29 | { 30 | "type": "link", 31 | "attributes": { 32 | "rel": "stylesheet", 33 | "href": "https://cdn.jsdelivr.net/gh/highlightjs/highlight.js@11.3.1/src/styles/github-dark.min.css" 34 | } 35 | } 36 | ] 37 | }, 38 | { 39 | "type": "meta", 40 | "attributes": { 41 | "name": "pagepath", 42 | "content": { 43 | "utility": "get", 44 | "parameters": ["context", "pagePath"] 45 | } 46 | } 47 | }, 48 | { 49 | "type": "meta", 50 | "attributes": { 51 | "charset": "UTF-8", 52 | "name": "viewport", 53 | "content": "width=device-width, initial-scale=1.0" 54 | } 55 | }, 56 | { 57 | "type": "meta", 58 | "attributes": { 59 | "name": "og:site_name", 60 | "content": { 61 | "utility": "get", 62 | "parameters": ["context", "meta.siteName"] 63 | } 64 | } 65 | }, 66 | { 67 | "type": "meta", 68 | "attributes": { 69 | "name": "twitter:site", 70 | "content": { 71 | "utility": "get", 72 | "parameters": ["context", "meta.siteName"] 73 | } 74 | } 75 | }, 76 | { 77 | "type": "meta", 78 | "attributes": { 79 | "name": "og:title", 80 | "content": { 81 | "utility": "get", 82 | "parameters": ["context", "meta.title"] 83 | } 84 | } 85 | }, 86 | { 87 | "type": "meta", 88 | "attributes": { 89 | "name": "twitter:title", 90 | "content": { 91 | "utility": "get", 92 | "parameters": ["context", "meta.title"] 93 | } 94 | } 95 | }, 96 | { 97 | "type": "meta", 98 | "attributes": { 99 | "name": "description", 100 | "content": { 101 | "utility": "get", 102 | "parameters": ["context", "meta.description"] 103 | } 104 | } 105 | }, 106 | { 107 | "type": "meta", 108 | "attributes": { 109 | "name": "og:description", 110 | "content": { 111 | "utility": "get", 112 | "parameters": ["context", "meta.description"] 113 | } 114 | } 115 | }, 116 | { 117 | "type": "meta", 118 | "attributes": { 119 | "name": "twitter:description", 120 | "content": { 121 | "utility": "get", 122 | "parameters": ["context", "meta.description"] 123 | } 124 | } 125 | }, 126 | { 127 | "type": "meta", 128 | "attributes": { 129 | "name": "built", 130 | "content": { 131 | "utility": "get", 132 | "parameters": ["context", "meta.built"] 133 | } 134 | } 135 | }, 136 | { 137 | "type": "title", 138 | "children": { 139 | "utility": "get", 140 | "parameters": ["context", "meta.title"] 141 | } 142 | } 143 | ] 144 | -------------------------------------------------------------------------------- /site/components/Navigation.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /site/components/Scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "foreach": [{ "utility": "get", "parameters": ["context", "scripts"] }, { 3 | "type": "script", 4 | "attributes": { 5 | "type": { 6 | "utility": "get", 7 | "parameters": ["props", "type"] 8 | }, 9 | "src": { 10 | "utility": "get", 11 | "parameters": ["props", "src"] 12 | } 13 | } 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /site/dataSources.ts: -------------------------------------------------------------------------------- 1 | import markdown from "./transforms/markdown.ts"; 2 | 3 | async function processMarkdown(filename: string) { 4 | return markdown(await Deno.readTextFile(filename)); 5 | } 6 | 7 | export { processMarkdown }; 8 | -------------------------------------------------------------------------------- /site/layouts/siteIndex.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "BaseLayout", 3 | "props": { 4 | "content": [ 5 | { 6 | "type": "header", 7 | "class": "bg-gradient-to-br from-blue-200 to-green-100 py-8", 8 | "children": [ 9 | { 10 | "type": "div", 11 | "class": "sm:mx-auto px-4 py-4 sm:py-8 max-w-3xl flex", 12 | "children": [ 13 | { 14 | "type": "div", 15 | "class": "flex flex-col gap-8", 16 | "children": [ 17 | { 18 | "type": "h1", 19 | "class": "text-4xl md:text-8xl", 20 | "children": [ 21 | { 22 | "type": "span", 23 | "class": "whitespace-nowrap pr-4", 24 | "children": "✥" 25 | }, 26 | { 27 | "type": "span", 28 | "children": "dragjs" 29 | } 30 | ] 31 | }, 32 | { 33 | "type": "h2", 34 | "class": "text-xl md:text-4xl font-extralight", 35 | "children": "Easy dragging with JavaScript" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | { 44 | "type": "div", 45 | "class": "md:mx-auto my-8 px-4 md:px-0 w-full lg:max-w-3xl", 46 | "children": { 47 | "utility": "get", 48 | "parameters": ["context", "readme.content"] 49 | } 50 | } 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /site/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | "siteName": "dragjs", 4 | "url": "http://bebraw.github.io/dragjs/" 5 | } 6 | -------------------------------------------------------------------------------- /site/pageUtilities.ts: -------------------------------------------------------------------------------- 1 | import md from "./transforms/markdown.ts"; 2 | import type { Context } from "https://deno.land/x/gustwind@v0.39.4/breezewind/types.ts"; 3 | import type { Routes } from "https://deno.land/x/gustwind@v0.39.4/types.ts"; 4 | 5 | function init({ routes }: { routes: Routes }) { 6 | async function processMarkdown(_: Context, input: string) { 7 | return (await md(input)).content; 8 | } 9 | 10 | function trim(_: Context, str: string, char: string) { 11 | if (!str) { 12 | throw new Error("No string to trim!"); 13 | } 14 | 15 | // Exception for / 16 | if (str === char) { 17 | return str; 18 | } 19 | 20 | // Adapted from https://www.sitepoint.com/trimming-strings-in-javascript/ 21 | return str.replace(new RegExp("^[" + char + "]+"), "").replace( 22 | new RegExp("[" + char + "]+$"), 23 | "", 24 | ); 25 | } 26 | 27 | function validateUrl(_: Context, url: string) { 28 | if (!url) { 29 | return; 30 | } 31 | 32 | if (routes[url]) { 33 | return url === "/" ? "/" : `/${url}/`; 34 | } 35 | 36 | // TODO: This would be a good spot to check the url doesn't 404 37 | // To keep this fast, some kind of local, time-based cache would 38 | // be good to have to avoid hitting the urls all the time. 39 | if (url.startsWith("http")) { 40 | return url; 41 | } 42 | 43 | throw new Error(`Failed to find matching url for "${url}"`); 44 | } 45 | 46 | return { processMarkdown, trim, validateUrl }; 47 | } 48 | 49 | export { init }; 50 | -------------------------------------------------------------------------------- /site/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "layout": "siteIndex", 4 | "meta": { 5 | "title": "dragjs", 6 | "description": "Simple drag utilities" 7 | }, 8 | "scripts": [{ "name": "demo", "srcPrefix": "./" }], 9 | "dataSources": [ 10 | { 11 | "operation": "processMarkdown", 12 | "parameters": ["./README.md"], 13 | "name": "readme" 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /site/scripts/demo.ts: -------------------------------------------------------------------------------- 1 | import { draggable, slider, xyslider } from "../../mod.ts"; 2 | 3 | window.onload = function () { 4 | const draggableElement = document.getElementById("draggable"); 5 | 6 | draggableElement && 7 | draggable({ 8 | element: draggableElement, 9 | }, { 10 | change: ({ x, y }) => { 11 | log("draggable x:" + x.toFixed(2) + ", y: " + y.toFixed(2)); 12 | 13 | return true; 14 | }, 15 | }); 16 | 17 | const draggableTwoElement = document.getElementById("draggabletwo"); 18 | const handle = draggableTwoElement?.children[0] as HTMLElement; 19 | 20 | draggableTwoElement && handle && draggable({ element: draggableTwoElement, handle }); 21 | 22 | const onedContainer = document.getElementById("onedContainer"); 23 | 24 | onedContainer && slider({ 25 | parent: onedContainer, 26 | "class": "oned", 27 | cbs: { 28 | begin: () => { 29 | log("2dslider: begin"); 30 | }, 31 | change: ({ x, pointer }) => { 32 | const newX = clamp(x * 100, 0, 100).toFixed(2) + "%"; 33 | 34 | log("2dslider: " + newX); 35 | 36 | if (pointer) { 37 | pointer.style.left = newX; 38 | } 39 | }, 40 | end: () => { 41 | log("2dslider: end"); 42 | }, 43 | }, 44 | }); 45 | 46 | const twodContainer = document.getElementById("twodContainer"); 47 | 48 | twodContainer && xyslider({ 49 | parent: twodContainer, 50 | "class": "twod", 51 | cbs: { 52 | change: ({ x, y, pointer }) => { 53 | const newX = clamp(x * 100, 0, 100).toFixed(2) + "%"; 54 | const newY = clamp(y * 100, 0, 100).toFixed(2) + "%"; 55 | 56 | log("x: " + newX + ", y: " + newY); 57 | 58 | if (pointer) { 59 | pointer.style.left = newX; 60 | pointer.style.top = newY; 61 | } 62 | }, 63 | }, 64 | }); 65 | }; 66 | 67 | function log(msg: string) { 68 | const valueElement = document.getElementById("value"); 69 | 70 | console.log(msg); 71 | 72 | if (valueElement) { 73 | valueElement.innerHTML = msg; 74 | } 75 | } 76 | 77 | function clamp(a: number, min: number, max: number) { 78 | return Math.min(Math.max(a, min), max); 79 | } 80 | -------------------------------------------------------------------------------- /site/transforms/markdown.ts: -------------------------------------------------------------------------------- 1 | import { marked } from "https://unpkg.com/marked@4.0.0/lib/marked.esm.js"; 2 | import { install, tw } from "https://esm.sh/@twind/core@1.1.1"; // 1.1.3 doesn't work! 3 | import highlight from "https://unpkg.com/@highlightjs/cdn-assets@11.3.1/es/core.min.js"; 4 | import highlightJS from "https://unpkg.com/highlight.js@11.3.1/es/languages/javascript"; 5 | import highlightTS from "https://unpkg.com/highlight.js@11.3.1/es/languages/typescript"; 6 | import highlightXML from "https://unpkg.com/highlight.js@11.3.1/es/languages/xml"; 7 | import twindSetup from '../twindSetup.ts' 8 | 9 | install(twindSetup); 10 | 11 | highlight.registerLanguage("js", highlightJS); 12 | highlight.registerLanguage("javascript", highlightJS); 13 | highlight.registerLanguage("html", highlightXML); 14 | highlight.registerLanguage("ts", highlightTS); 15 | highlight.registerLanguage("typescript", highlightTS); 16 | 17 | marked.setOptions({ 18 | gfm: true, 19 | breaks: false, 20 | pedantic: false, 21 | sanitize: false, 22 | smartLists: true, 23 | smartypants: true, 24 | highlight: (code: string, language: string) => 25 | highlight.highlight(code, { language }).value, 26 | }); 27 | 28 | function transformMarkdown(input: string) { 29 | // https://github.com/markedjs/marked/issues/545 30 | const tableOfContents: { slug: string; level: number; text: string }[] = []; 31 | 32 | // https://marked.js.org/using_pro#renderer 33 | // https://github.com/markedjs/marked/blob/master/src/Renderer.js 34 | marked.use({ 35 | renderer: { 36 | code(code: string, infostring: string): string { 37 | const lang = ((infostring || "").match(/\S*/) || [])[0]; 38 | 39 | // @ts-ignore How to type this? 40 | if (this.options.highlight) { 41 | // @ts-ignore How to type this? 42 | const out = this.options.highlight(code, lang); 43 | 44 | if (out != null && out !== code) { 45 | code = out; 46 | } 47 | } 48 | 49 | code = code.replace(/\n$/, "") + "\n"; 50 | 51 | if (!lang) { 52 | return "
" +
 53 |             code +
 54 |             "
\n"; 55 | } 56 | 57 | return '
' +
 64 |           code +
 65 |           "
\n"; 66 | }, 67 | paragraph(text: string) { 68 | return '

' + text + "

"; 69 | }, 70 | heading( 71 | text: string, 72 | level: number, 73 | raw: string, 74 | slugger: { slug: (s: string) => string }, 75 | ) { 76 | const slug = slugger.slug(raw); 77 | 78 | tableOfContents.push({ slug, level, text }); 79 | 80 | const classes = { 81 | 1: "inline-block underline text-gray-900 font-extrabold leading-3 text-3xl mt-0 mb-8", 82 | 2: "inline-block underline text-gray-900 font-bold leading-4 text-xl mt-4 mb-2", 83 | 3: "inline-block underline text-gray-900 font-semibold leading-5 text-lg mt-1 mb-1", 84 | 4: "inline-block underline text-gray-900 font-medium leading-6 mt-1 mb-0.5", 85 | }; 86 | 87 | return ( 88 | '' + 100 | text + 101 | "" + 104 | "\n" 105 | ); 106 | }, 107 | link(href: string, title: string, text: string) { 108 | if (href === null) { 109 | return text; 110 | } 111 | let out = '"; 116 | return out; 117 | }, 118 | list(body: string, ordered: string, start: number) { 119 | const type = ordered ? "ol" : "ul", 120 | startatt = ordered && start !== 1 ? ' start="' + start + '"' : "", 121 | klass = ordered 122 | ? "list-decimal list-outside my-2" 123 | : "list-disc list-outside my-2"; 124 | return ( 125 | "<" + 126 | type + 127 | startatt + 128 | ' class="' + 129 | tw(klass) + 130 | '">\n' + 131 | body + 132 | "\n" 135 | ); 136 | }, 137 | }, 138 | }); 139 | 140 | return { content: marked(input), tableOfContents }; 141 | } 142 | 143 | export default transformMarkdown; 144 | -------------------------------------------------------------------------------- /site/twindSetup.ts: -------------------------------------------------------------------------------- 1 | import presetAutoprefix from "https://esm.sh/@twind/preset-autoprefix@1.0.5"; 2 | import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.1"; 3 | 4 | export default { 5 | presets: [presetAutoprefix(), presetTailwind()], 6 | }; 7 | --------------------------------------------------------------------------------