Compare commits
33 commits
developmen
...
prosemirro
Author | SHA1 | Date | |
---|---|---|---|
|
d34f837e19 | ||
|
264966de02 | ||
|
8b4f112462 | ||
|
20f37292a1 | ||
|
b1f5495a7f | ||
|
bb12541179 | ||
|
e3ead1c115 | ||
|
9b4ea368dc | ||
|
4b08eef12c | ||
|
b2283106fc | ||
|
7125530e55 | ||
|
7622106665 | ||
|
89194a3f85 | ||
|
7703face52 | ||
|
c013d7e549 | ||
|
07c8876e22 | ||
|
0dc64d22ef | ||
|
013943dcc5 | ||
|
dc1c9807ef | ||
|
56d7864bdf | ||
|
1018b5627e | ||
|
717557df89 | ||
|
6744ab2ff9 | ||
|
4e5153d372 | ||
|
34db138a64 | ||
|
c3595b1807 | ||
|
a8f48185b5 | ||
|
9d7174557e | ||
|
47c3d4fc0f | ||
|
81dfe9c345 | ||
|
0fb8ba00a5 | ||
|
aa9fe9ca82 | ||
|
27f9e8e4bd |
41 changed files with 4668 additions and 0 deletions
49
TODO
Normal file
49
TODO
Normal file
|
@ -0,0 +1,49 @@
|
|||
### Next
|
||||
|
||||
- Table cell height resize & cell width resize via width style
|
||||
- Column resize source: https://github.com/ProseMirror/prosemirror-tables/blob/master/src/columnresizing.js
|
||||
- Have updated column resizing to set cell widths
|
||||
- Now need to handle table overall size on change, then heights.
|
||||
|
||||
- Details/Summary
|
||||
- Need view to control summary editability, make readonly but editable via popover.
|
||||
- Need some default styles to visualise details boundary.
|
||||
- Markdown parser needs to be updated to handle separate open/close tags for blocks.
|
||||
|
||||
### In-Progress
|
||||
|
||||
- Tables
|
||||
- Details/Summary
|
||||
|
||||
### Features
|
||||
|
||||
- Images
|
||||
- Drawings
|
||||
- LTR/RTL control
|
||||
- Fullscreen
|
||||
- Paste Image Uploading
|
||||
- Drag + Drop Image Uploading
|
||||
- Checkbox/TODO list items
|
||||
- Code blocks
|
||||
- Indents
|
||||
- Attachment integration (Drag & drop)
|
||||
- Template system integration.
|
||||
|
||||
### Improvements
|
||||
|
||||
- List type changing.
|
||||
- Color picker options should have "clear" option.
|
||||
- Color picker buttons should be split, with button to re-apply last selected color.
|
||||
- Color picker options should change color if different instead of remove.
|
||||
- Clear formatting, If no selection range, clear the formatting of parent block.
|
||||
- If no marks, clear the block type if text type?
|
||||
- Remove links button? (Action already in place if link href is empty).
|
||||
- Links - Validate URL.
|
||||
- Links - Integrate entity picker.
|
||||
- iFrame - Parse iframe HTML & auto-convert youtube/vimeo urls to embeds.
|
||||
|
||||
### Notes
|
||||
|
||||
- Use NodeViews for embedded content (Code, Drawings) where control is needed.
|
||||
- Probably still easiest to have seperate (codemirror) MD editor. Can alter display output via NodeViews to make MD like
|
||||
but its tricky since editing the markdown content would change the block definition/type while editing.
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use Exception;
|
||||
|
@ -16,6 +17,7 @@ class UserAvatars
|
|||
{
|
||||
$this->imageService = $imageService;
|
||||
$this->http = $http;
|
||||
$ldapService = app()->make(LdapService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
432
package-lock.json
generated
432
package-lock.json
generated
|
@ -7,9 +7,18 @@
|
|||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.63.3",
|
||||
"crelt": "^1.0.5",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"prosemirror-commands": "^1.1.12",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.6.0",
|
||||
"prosemirror-model": "^1.15.0",
|
||||
"prosemirror-schema-list": "^1.1.6",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-tables": "^1.1.1",
|
||||
"prosemirror-view": "^1.23.2",
|
||||
"sortablejs": "^1.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -219,6 +228,11 @@
|
|||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
|
||||
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
|
@ -1227,6 +1241,11 @@
|
|||
"integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz",
|
||||
"integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ=="
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
|
@ -1345,6 +1364,193 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.12.tgz",
|
||||
"integrity": "sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-6+YwTjmqDwlA/Dm+5wK67ezgqgjA/MhSDgaNxKUzH97SmeuWFXyLeDRxxOPZeSo7yTxcDGUCWTEjmQZsVBuMrQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-example-setup": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.1.2.tgz",
|
||||
"integrity": "sha512-MTpIMyqk08jFnzxeRMCinCEMtVSTUtxKgQBGxfCbVe9C6zIOqp9qZZJz5Ojaad1GETySyuj8+OIHHvQsIaaaGQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-dropcursor": "^1.0.0",
|
||||
"prosemirror-gapcursor": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-inputrules": "^1.0.0",
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-menu": "^1.0.0",
|
||||
"prosemirror-schema-list": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.2.0.tgz",
|
||||
"integrity": "sha512-yCLy5+0rVqLir/KcHFathQj4Rf8aRHi80FmEfKtM0JmyzvwdomslLzDZ/pX4oFhFKDgjl/WBBBFNqDyNifWg7g==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.2.0.tgz",
|
||||
"integrity": "sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz",
|
||||
"integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz",
|
||||
"integrity": "sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==",
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.6.0.tgz",
|
||||
"integrity": "sha512-y/gRpJIIrNArtkyMax7ypYafb+ZMjddbVHI+AwlcUfCLCCXK57cOmfBMKYVq9kdEKJYVdYHdoyWsVNn1nWLHUg==",
|
||||
"dependencies": {
|
||||
"markdown-it": "^10.0.0",
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/linkify-it": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown/node_modules/markdown-it": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.js"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.1.4.tgz",
|
||||
"integrity": "sha512-2ROsji/X9ciDnVSRvSTqFygI34GEdHfQSsK4zBKjPxSEroeiHHcdRMS1ofNIf2zM0Vpp5/YqfpxynElymQkqzg==",
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.15.0.tgz",
|
||||
"integrity": "sha512-hQJv7SnIhlAy9ga3lhPPgaufhvCbQB9tHwscJ9E1H1pPHmN8w5V/lURueoYv9Kc3/bpNWoyHa8r3g//m7N0ChQ==",
|
||||
"dependencies": {
|
||||
"orderedmap": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.6.tgz",
|
||||
"integrity": "sha512-aFGEdaCWmJzouZ8DwedmvSsL50JpRkqhQ6tcpThwJONVVmCgI36LJHtoQ4VGZbusMavaBhXXr33zyD2IVsTlkw==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz",
|
||||
"integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
|
||||
"integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.1.2",
|
||||
"prosemirror-model": "^1.8.1",
|
||||
"prosemirror-state": "^1.3.1",
|
||||
"prosemirror-transform": "^1.2.1",
|
||||
"prosemirror-view": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
|
||||
"integrity": "sha512-9NLVXy1Sfa2G6qPqhWMkEvwQQMTw7OyTqOZbJaGQWsCeH3hH5Cw+c5eNaLM1Uu75EyKLsEZhJ93XpHJBa6RX8A==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.23.2.tgz",
|
||||
"integrity": "sha512-iPgRw6tpcN+KH1yKmSnRmDKsJBVkWLFP6laHcz9rh/n0Ndz7YKKCDldtw6FhHBYoWmZeubbhV/rrQW0VCDG9iw==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
|
@ -1408,6 +1614,11 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz",
|
||||
"integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
|
||||
|
@ -1521,6 +1732,11 @@
|
|||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
|
@ -1658,6 +1874,11 @@
|
|||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
|
@ -1926,6 +2147,11 @@
|
|||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"crelt": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
|
||||
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
|
@ -2630,6 +2856,11 @@
|
|||
"integrity": "sha512-k41FwbcLnlgnFh69f4qdUfvDQ+5vaSDnVPFI/y5XuhKRq97EnVVneO9F1ESVCdiVu4fCS2L8usX3mU331hB7pg==",
|
||||
"dev": true
|
||||
},
|
||||
"orderedmap": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.1.tgz",
|
||||
"integrity": "sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ=="
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
|
@ -2709,6 +2940,192 @@
|
|||
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
|
||||
"dev": true
|
||||
},
|
||||
"prosemirror-commands": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.12.tgz",
|
||||
"integrity": "sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-dropcursor": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.4.0.tgz",
|
||||
"integrity": "sha512-6+YwTjmqDwlA/Dm+5wK67ezgqgjA/MhSDgaNxKUzH97SmeuWFXyLeDRxxOPZeSo7yTxcDGUCWTEjmQZsVBuMrQ==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-example-setup": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-example-setup/-/prosemirror-example-setup-1.1.2.tgz",
|
||||
"integrity": "sha512-MTpIMyqk08jFnzxeRMCinCEMtVSTUtxKgQBGxfCbVe9C6zIOqp9qZZJz5Ojaad1GETySyuj8+OIHHvQsIaaaGQ==",
|
||||
"requires": {
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-dropcursor": "^1.0.0",
|
||||
"prosemirror-gapcursor": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-inputrules": "^1.0.0",
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-menu": "^1.0.0",
|
||||
"prosemirror-schema-list": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-gapcursor": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.2.0.tgz",
|
||||
"integrity": "sha512-yCLy5+0rVqLir/KcHFathQj4Rf8aRHi80FmEfKtM0JmyzvwdomslLzDZ/pX4oFhFKDgjl/WBBBFNqDyNifWg7g==",
|
||||
"requires": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-history": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.2.0.tgz",
|
||||
"integrity": "sha512-B9v9xtf4fYbKxQwIr+3wtTDNLDZcmMMmGiI3TAPShnUzvo+Rmv1GiUrsQChY1meetHl7rhML2cppF3FTs7f7UQ==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-inputrules": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz",
|
||||
"integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-keymap": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz",
|
||||
"integrity": "sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==",
|
||||
"requires": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-markdown": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.6.0.tgz",
|
||||
"integrity": "sha512-y/gRpJIIrNArtkyMax7ypYafb+ZMjddbVHI+AwlcUfCLCCXK57cOmfBMKYVq9kdEKJYVdYHdoyWsVNn1nWLHUg==",
|
||||
"requires": {
|
||||
"markdown-it": "^10.0.0",
|
||||
"prosemirror-model": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"requires": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
|
||||
"integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
|
||||
"integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"entities": "~2.0.0",
|
||||
"linkify-it": "^2.0.0",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"prosemirror-menu": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.1.4.tgz",
|
||||
"integrity": "sha512-2ROsji/X9ciDnVSRvSTqFygI34GEdHfQSsK4zBKjPxSEroeiHHcdRMS1ofNIf2zM0Vpp5/YqfpxynElymQkqzg==",
|
||||
"requires": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-model": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.15.0.tgz",
|
||||
"integrity": "sha512-hQJv7SnIhlAy9ga3lhPPgaufhvCbQB9tHwscJ9E1H1pPHmN8w5V/lURueoYv9Kc3/bpNWoyHa8r3g//m7N0ChQ==",
|
||||
"requires": {
|
||||
"orderedmap": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-schema-list": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.1.6.tgz",
|
||||
"integrity": "sha512-aFGEdaCWmJzouZ8DwedmvSsL50JpRkqhQ6tcpThwJONVVmCgI36LJHtoQ4VGZbusMavaBhXXr33zyD2IVsTlkw==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-state": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz",
|
||||
"integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-tables": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.1.1.tgz",
|
||||
"integrity": "sha512-LmCz4jrlqQZRsYRDzCRYf/pQ5CUcSOyqZlAj5kv67ZWBH1SVLP2U9WJEvQfimWgeRlIz0y0PQVqO1arRm1+woA==",
|
||||
"requires": {
|
||||
"prosemirror-keymap": "^1.1.2",
|
||||
"prosemirror-model": "^1.8.1",
|
||||
"prosemirror-state": "^1.3.1",
|
||||
"prosemirror-transform": "^1.2.1",
|
||||
"prosemirror-view": "^1.13.3"
|
||||
}
|
||||
},
|
||||
"prosemirror-transform": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.3.tgz",
|
||||
"integrity": "sha512-9NLVXy1Sfa2G6qPqhWMkEvwQQMTw7OyTqOZbJaGQWsCeH3hH5Cw+c5eNaLM1Uu75EyKLsEZhJ93XpHJBa6RX8A==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prosemirror-view": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.23.2.tgz",
|
||||
"integrity": "sha512-iPgRw6tpcN+KH1yKmSnRmDKsJBVkWLFP6laHcz9rh/n0Ndz7YKKCDldtw6FhHBYoWmZeubbhV/rrQW0VCDG9iw==",
|
||||
"requires": {
|
||||
"prosemirror-model": "^1.14.3",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
|
@ -2757,6 +3174,11 @@
|
|||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"rope-sequence": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.2.tgz",
|
||||
"integrity": "sha512-ku6MFrwEVSVmXLvy3dYph3LAMNS0890K7fabn+0YIRQ2T96T9F4gkFf0vf0WW0JUraNWwGRtInEpH7yO4tbQZg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.43.4",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.43.4.tgz",
|
||||
|
@ -2852,6 +3274,11 @@
|
|||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||
"dev": true
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
|
@ -2959,6 +3386,11 @@
|
|||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"w3c-keyname": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
|
||||
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw=="
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
|
|
11
package.json
11
package.json
|
@ -7,6 +7,8 @@
|
|||
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main",
|
||||
"build:js:watch": "chokidar --initial \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2019 --main-fields=module,main --minify",
|
||||
"build:js_editor:dev": "esbuild --bundle ./resources/js/editor.js --outfile=public/dist/editor.js --sourcemap --target=es2019 --main-fields=module,main",
|
||||
"build:js_editor:watch": "chokidar --initial \"./resources/js/editor.js\" \"./resources/js/editor/**/*.js\" -c \"npm run build:js_editor:dev\"",
|
||||
"build": "npm-run-all --parallel build:*:dev",
|
||||
"production": "npm-run-all --parallel build:*:production",
|
||||
"dev": "npm-run-all --parallel watch livereload",
|
||||
|
@ -25,9 +27,18 @@
|
|||
"dependencies": {
|
||||
"clipboard": "^2.0.8",
|
||||
"codemirror": "^5.63.3",
|
||||
"crelt": "^1.0.5",
|
||||
"dropzone": "^5.9.3",
|
||||
"markdown-it": "^12.2.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
"prosemirror-commands": "^1.1.12",
|
||||
"prosemirror-example-setup": "^1.1.2",
|
||||
"prosemirror-markdown": "^1.6.0",
|
||||
"prosemirror-model": "^1.15.0",
|
||||
"prosemirror-schema-list": "^1.1.6",
|
||||
"prosemirror-state": "^1.3.4",
|
||||
"prosemirror-tables": "^1.1.1",
|
||||
"prosemirror-view": "^1.23.2",
|
||||
"sortablejs": "^1.14.0"
|
||||
}
|
||||
}
|
||||
|
|
18
resources/js/editor.js
Normal file
18
resources/js/editor.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import MarkdownView from "./editor/MarkdownView";
|
||||
import ProseMirrorView from "./editor/ProseMirrorView";
|
||||
|
||||
// Next step: https://prosemirror.net/examples/menu/
|
||||
|
||||
const place = document.querySelector("#editor");
|
||||
let view = new ProseMirrorView(place, document.getElementById('content').innerHTML);
|
||||
|
||||
const markdownToggle = document.getElementById('markdown-toggle');
|
||||
markdownToggle.addEventListener('change', event => {
|
||||
const View = markdownToggle.checked ? MarkdownView : ProseMirrorView;
|
||||
if (view instanceof View) return
|
||||
const content = view.content
|
||||
console.log(content);
|
||||
view.destroy()
|
||||
view = new View(place, content)
|
||||
view.focus()
|
||||
});
|
28
resources/js/editor/MarkdownView.js
Normal file
28
resources/js/editor/MarkdownView.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import {htmlToDoc, docToHtml} from "./util";
|
||||
|
||||
import parser from "./markdown-parser";
|
||||
import serializer from "./markdown-serializer";
|
||||
|
||||
class MarkdownView {
|
||||
constructor(target, content) {
|
||||
// Build DOM from content
|
||||
const htmlDoc = htmlToDoc(content);
|
||||
const markdown = serializer.serialize(htmlDoc);
|
||||
|
||||
this.textarea = target.appendChild(document.createElement("textarea"))
|
||||
this.textarea.value = markdown;
|
||||
this.textarea.style.width = '1000px';
|
||||
this.textarea.style.height = '1000px';
|
||||
}
|
||||
|
||||
get content() {
|
||||
const markdown = this.textarea.value;
|
||||
const doc = parser.parse(markdown);
|
||||
return docToHtml(doc);
|
||||
}
|
||||
|
||||
focus() { this.textarea.focus() }
|
||||
destroy() { this.textarea.remove() }
|
||||
}
|
||||
|
||||
export default MarkdownView;
|
52
resources/js/editor/ProseMirrorView.js
Normal file
52
resources/js/editor/ProseMirrorView.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import {EditorState} from "prosemirror-state";
|
||||
import {EditorView} from "prosemirror-view";
|
||||
import {exampleSetup} from "prosemirror-example-setup";
|
||||
import {tableEditing} from "prosemirror-tables";
|
||||
|
||||
import {DOMParser} from "prosemirror-model";
|
||||
|
||||
import schema from "./schema";
|
||||
import menu from "./menu";
|
||||
import nodeViews from "./node-views";
|
||||
import {stateToHtml} from "./util";
|
||||
import {columnResizing} from "./plugins/table-resizing";
|
||||
|
||||
class ProseMirrorView {
|
||||
constructor(target, content) {
|
||||
|
||||
// Build DOM from content
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.innerHTML = content;
|
||||
|
||||
this.view = new EditorView(target, {
|
||||
state: EditorState.create({
|
||||
doc: DOMParser.fromSchema(schema).parse(renderDoc.body),
|
||||
plugins: [
|
||||
...exampleSetup({schema, menuBar: false}),
|
||||
menu,
|
||||
columnResizing(),
|
||||
tableEditing(),
|
||||
]
|
||||
}),
|
||||
nodeViews,
|
||||
});
|
||||
|
||||
// Fix for native handles (Such as table size handling) in some browsers
|
||||
document.execCommand("enableObjectResizing", false, "false")
|
||||
document.execCommand("enableInlineTableEditing", false, "false")
|
||||
}
|
||||
|
||||
get content() {
|
||||
return stateToHtml(this.view.state);
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.view.focus()
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.view.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
export default ProseMirrorView;
|
102
resources/js/editor/commands.js
Normal file
102
resources/js/editor/commands.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* @param {String} attrName
|
||||
* @param {String} attrValue
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function setBlockAttr(attrName, attrValue) {
|
||||
return function (state, dispatch) {
|
||||
const ref = state.selection;
|
||||
const from = ref.from;
|
||||
const to = ref.to;
|
||||
let applicable = false;
|
||||
|
||||
state.doc.nodesBetween(from, to, function (node, pos) {
|
||||
if (applicable) {
|
||||
return false
|
||||
}
|
||||
if (!node.isTextblock || node.attrs[attrName] === attrValue) {
|
||||
return
|
||||
}
|
||||
|
||||
applicable = node.attrs[attrName] !== undefined;
|
||||
});
|
||||
|
||||
if (!applicable) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (dispatch) {
|
||||
const tr = state.tr;
|
||||
tr.doc.nodesBetween(from, to, function (node, pos) {
|
||||
const nodeAttrs = Object.assign({}, node.attrs);
|
||||
if (node.attrs[attrName] !== undefined) {
|
||||
nodeAttrs[attrName] = attrValue;
|
||||
tr.setBlockType(pos, pos + 1, node.type, nodeAttrs)
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmNodeType} blockType
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function insertBlockBefore(blockType) {
|
||||
return function (state, dispatch) {
|
||||
const startPosition = state.selection.$from.before(1);
|
||||
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.insert(startPosition, blockType.create()));
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} rows
|
||||
* @param {Number} columns
|
||||
* @param {Object} tableAttrs
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function insertTable(rows, columns, tableAttrs) {
|
||||
return function (state, dispatch) {
|
||||
if (!dispatch) return true;
|
||||
|
||||
const tr = state.tr;
|
||||
const nodes = state.schema.nodes;
|
||||
|
||||
const rowNodes = [];
|
||||
for (let y = 0; y < rows; y++) {
|
||||
const rowCells = [];
|
||||
for (let x = 0; x < columns; x++) {
|
||||
const cellText = nodes.paragraph.create(null);
|
||||
rowCells.push(nodes.table_cell.create(null, cellText));
|
||||
}
|
||||
rowNodes.push(nodes.table_row.create(null, rowCells));
|
||||
}
|
||||
|
||||
const table = nodes.table.create(tableAttrs, rowNodes);
|
||||
tr.replaceSelectionWith(table);
|
||||
dispatch(tr);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {PmCommandHandler}
|
||||
*/
|
||||
export function removeMarks() {
|
||||
return function (state, dispatch) {
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.removeMark(state.selection.from, state.selection.to, null));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
69
resources/js/editor/markdown-parser.js
Normal file
69
resources/js/editor/markdown-parser.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import schema from "./schema";
|
||||
import markdownit from "markdown-it";
|
||||
import {MarkdownParser, defaultMarkdownParser} from "prosemirror-markdown";
|
||||
import {htmlToDoc, KeyedMultiStack} from "./util";
|
||||
|
||||
const tokens = defaultMarkdownParser.tokens;
|
||||
|
||||
// These are really a placeholder on the object to allow the below
|
||||
// parser.tokenHandlers.html_[block/inline] hacks to work as desired.
|
||||
tokens.html_block = {block: "callout", noCloseToken: true};
|
||||
tokens.html_inline = {mark: "underline"};
|
||||
|
||||
const tokenizer = markdownit("commonmark", {html: true});
|
||||
const parser = new MarkdownParser(schema, tokenizer, tokens);
|
||||
|
||||
// When we come across HTML blocks we use the document schema to parse them
|
||||
// into nodes then re-add those back into the parser state.
|
||||
parser.tokenHandlers.html_block = function(state, tok, tokens, i) {
|
||||
const contentDoc = htmlToDoc(tok.content || '');
|
||||
for (const node of contentDoc.content.content) {
|
||||
state.addNode(node.type, node.attrs, node.content);
|
||||
}
|
||||
};
|
||||
|
||||
// When we come across inline HTML we parse out the tag and keep track of
|
||||
// that in a stack, along with the marks they parse out to.
|
||||
// We open/close the marks within the state depending on the tag open/close type.
|
||||
const tagStack = new KeyedMultiStack();
|
||||
parser.tokenHandlers.html_inline = function(state, tok, tokens, i) {
|
||||
const isClosing = tok.content.startsWith('</');
|
||||
const isSelfClosing = tok.content.endsWith('/>');
|
||||
const tagName = parseTagNameFromHtmlTokenContent(tok.content);
|
||||
|
||||
if (!isClosing) {
|
||||
const completeTag = isSelfClosing ? tok.content : `${tok.content}a</${tagName}>`;
|
||||
const marks = extractMarksFromHtml(completeTag);
|
||||
tagStack.push(tagName, marks);
|
||||
for (const mark of marks) {
|
||||
state.openMark(mark);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelfClosing || isClosing) {
|
||||
const marks = (tagStack.pop(tagName) || []).reverse();
|
||||
for (const mark of marks) {
|
||||
state.closeMark(mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} html
|
||||
* @return {PmMark[]}
|
||||
*/
|
||||
function extractMarksFromHtml(html) {
|
||||
const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>');
|
||||
const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks;
|
||||
return marks || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tokenContent
|
||||
* @return {string}
|
||||
*/
|
||||
function parseTagNameFromHtmlTokenContent(tokenContent) {
|
||||
return tokenContent.split(' ')[0].replace(/[<>\/]/g, '').toLowerCase();
|
||||
}
|
||||
|
||||
export default parser;
|
138
resources/js/editor/markdown-serializer.js
Normal file
138
resources/js/editor/markdown-serializer.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
import {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "prosemirror-markdown";
|
||||
import {docToHtml} from "./util";
|
||||
|
||||
const nodes = defaultMarkdownSerializer.nodes;
|
||||
const marks = defaultMarkdownSerializer.marks;
|
||||
|
||||
|
||||
nodes.callout = function (state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
nodes.table = function (state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
nodes.iframe = function (state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
nodes.details = function (state, node) {
|
||||
wrapNodeWithHtml(state, node, '<details>', '</details>');
|
||||
};
|
||||
|
||||
nodes.details_summary = function(state, node) {
|
||||
writeNodeAsHtml(state, node);
|
||||
};
|
||||
|
||||
function isPlainURL(link, parent, index, side) {
|
||||
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) {
|
||||
return false
|
||||
}
|
||||
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||
if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) {
|
||||
return false
|
||||
}
|
||||
if (index == (side < 0 ? 1 : parent.childCount - 1)) {
|
||||
return true
|
||||
}
|
||||
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||
return !link.isInSet(next.marks)
|
||||
}
|
||||
|
||||
marks.link = {
|
||||
open(state, mark, parent, index) {
|
||||
const attrs = mark.attrs;
|
||||
if (attrs.target) {
|
||||
return `<a href="${attrs.target}" ${attrs.title ? `title="${attrs.title}"` : ''} target="${attrs.target}">`
|
||||
}
|
||||
return isPlainURL(mark, parent, index, 1) ? "<" : "["
|
||||
},
|
||||
close(state, mark, parent, index) {
|
||||
if (mark.attrs.target) {
|
||||
return `</a>`;
|
||||
}
|
||||
return isPlainURL(mark, parent, index, -1) ? ">"
|
||||
: "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")"
|
||||
}
|
||||
};
|
||||
|
||||
marks.underline = {
|
||||
open: '<span style="text-decoration: underline;">',
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
marks.strike = {
|
||||
open: '<span style="text-decoration: line-through;">',
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
marks.superscript = {
|
||||
open: '<sup>',
|
||||
close: '</sup>',
|
||||
};
|
||||
|
||||
marks.subscript = {
|
||||
open: '<sub>',
|
||||
close: '</sub>',
|
||||
};
|
||||
|
||||
marks.text_color = {
|
||||
open(state, mark, parent, index) {
|
||||
return `<span style="color: ${mark.attrs.color};">`
|
||||
},
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
marks.background_color = {
|
||||
open(state, mark, parent, index) {
|
||||
return `<span style="background-color: ${mark.attrs.color};">`
|
||||
},
|
||||
close: '</span>',
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {MarkdownSerializerState} state
|
||||
* @param {PmNode} node
|
||||
*/
|
||||
function writeNodeAsHtml(state, node) {
|
||||
const html = docToHtml({content: [node]});
|
||||
state.write(html);
|
||||
state.ensureNewLine();
|
||||
state.write('\n');
|
||||
state.closeBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MarkdownSerializerState} state
|
||||
* @param {PmNode} node
|
||||
* @param {String} openTag
|
||||
* @param {String} closeTag
|
||||
*/
|
||||
function wrapNodeWithHtml(state, node, openTag, closeTag) {
|
||||
state.write(openTag);
|
||||
state.ensureNewLine();
|
||||
state.renderContent(node);
|
||||
state.write(closeTag);
|
||||
state.closeBlock();
|
||||
state.ensureNewLine();
|
||||
state.write('\n');
|
||||
}
|
||||
|
||||
// Update serializers to just write out as HTML if we have an attribute
|
||||
// or element that cannot be represented in commonmark without losing
|
||||
// formatting or content.
|
||||
for (const [nodeType, serializerFunction] of Object.entries(nodes)) {
|
||||
nodes[nodeType] = function (state, node, parent, index) {
|
||||
if (node.attrs.align || node.attrs.height || node.attrs.width) {
|
||||
writeNodeAsHtml(state, node);
|
||||
} else {
|
||||
serializerFunction(state, node, parent, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const serializer = new MarkdownSerializer(nodes, marks);
|
||||
|
||||
export default serializer;
|
62
resources/js/editor/menu/ColorPickerGrid.js
Normal file
62
resources/js/editor/menu/ColorPickerGrid.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import crel from "crelt"
|
||||
import {prefix} from "./menu-utils";
|
||||
import {TextSelection} from "prosemirror-state"
|
||||
import {expandSelectionToMark} from "../util";
|
||||
|
||||
|
||||
class ColorPickerGrid {
|
||||
|
||||
constructor(markType, attrName, colors) {
|
||||
this.markType = markType;
|
||||
this.colors = colors
|
||||
this.attrName = attrName;
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
|
||||
const colorElems = [];
|
||||
for (const color of this.colors) {
|
||||
const elem = crel("div", {class: prefix + "-color-grid-item", style: `background-color: ${color};`});
|
||||
colorElems.push(elem);
|
||||
}
|
||||
|
||||
const wrap = crel("div", {class: prefix + "-color-grid-container"}, colorElems);
|
||||
wrap.addEventListener('click', event => {
|
||||
if (event.target.classList.contains(prefix + "-color-grid-item")) {
|
||||
const color = event.target.style.backgroundColor;
|
||||
this.onColorSelect(view, color);
|
||||
}
|
||||
});
|
||||
|
||||
function update(state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
onColorSelect(view, color) {
|
||||
const attrs = {[this.attrName]: color};
|
||||
const selection = view.state.selection;
|
||||
const {from, to} = expandSelectionToMark(view.state, selection, this.markType);
|
||||
const tr = view.state.tr;
|
||||
|
||||
const currentColorMarks = selection.$from.marksAcross(selection.$to) || [];
|
||||
const activeRelevantMark = currentColorMarks.filter(mark => {
|
||||
return mark.type === this.markType;
|
||||
})[0];
|
||||
const colorIsActive = activeRelevantMark && activeRelevantMark.attrs[this.attrName] === color;
|
||||
|
||||
tr.removeMark(from, to, this.markType);
|
||||
if (!colorIsActive) {
|
||||
tr.addMark(from, to, this.markType.create(attrs));
|
||||
}
|
||||
|
||||
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
||||
view.dispatch(tr);
|
||||
}
|
||||
}
|
||||
|
||||
export default ColorPickerGrid;
|
59
resources/js/editor/menu/DialogBox.js
Normal file
59
resources/js/editor/menu/DialogBox.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, renderItems} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
import {getIcon, icons} from "./icons";
|
||||
|
||||
class DialogBox {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show on the dialog.
|
||||
// **`closer`**`: function`
|
||||
// : The function to run when the dialog should close.
|
||||
constructor(content, options) {
|
||||
this.options = options || {};
|
||||
this.content = Array.isArray(content) ? content : [content];
|
||||
|
||||
this.closeMouseDownListener = null;
|
||||
this.wrap = null;
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const items = renderItems(this.content, view)
|
||||
|
||||
const titleText = crel("div", {class: prefix + "-dialog-title-text"}, this.options.label);
|
||||
const titleClose = crel("button", {class: prefix + "-dialog-title-close primary-background", type: "button"}, getIcon(icons.close));
|
||||
const titleContent = crel("div", {class: prefix + "-dialog-title"}, titleText, titleClose);
|
||||
const dialog = crel("div", {class: prefix + "-dialog"}, titleContent,
|
||||
crel("div", {class: prefix + "-dialog-content"}, items.dom));
|
||||
const wrap = crel("div", {class: prefix + "-dialog-wrap"}, dialog);
|
||||
this.wrap = wrap;
|
||||
|
||||
this.closeMouseDownListener = (event) => {
|
||||
if (!dialog.contains(event.target) || titleClose.contains(event.target)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
wrap.addEventListener("click", this.closeMouseDownListener);
|
||||
|
||||
function update(state) {
|
||||
let inner = items.update(state)
|
||||
wrap.style.display = inner ? "" : "none"
|
||||
return inner;
|
||||
}
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.options.closer) {
|
||||
this.options.closer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DialogBox;
|
51
resources/js/editor/menu/DialogForm.js
Normal file
51
resources/js/editor/menu/DialogForm.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, renderItems} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogForm {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`action`**`: function(FormData)`
|
||||
// : The submission action to run when the form is submitted.
|
||||
// **`canceler`**`: function`
|
||||
// : The cancel action to run when the form is cancelled.
|
||||
constructor(content, options) {
|
||||
this.options = options || {};
|
||||
this.content = Array.isArray(content) ? content : [content];
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const items = renderItems(this.content, view)
|
||||
|
||||
const formButtonCancel = crel("button", {class: prefix + "-dialog-button", type: "button"}, "Cancel");
|
||||
const formButtonSave = crel("button", {class: prefix + "-dialog-button", type: "submit"}, "Save");
|
||||
const footer = crel("div", {class: prefix + "-dialog-footer"}, formButtonCancel, formButtonSave);
|
||||
const form = crel("form", {class: prefix + "-dialog-form", action: '#'}, items.dom, footer);
|
||||
|
||||
form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
if (this.options.action) {
|
||||
this.options.action(new FormData(form));
|
||||
}
|
||||
});
|
||||
|
||||
formButtonCancel.addEventListener('click', event => {
|
||||
if (this.options.canceler) {
|
||||
this.options.canceler();
|
||||
}
|
||||
});
|
||||
|
||||
function update(state) {
|
||||
return items.update(state);
|
||||
}
|
||||
|
||||
return {dom: form, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogForm;
|
42
resources/js/editor/menu/DialogInput.js
Normal file
42
resources/js/editor/menu/DialogInput.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, randHtmlId} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogInput {
|
||||
// :: (?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show for the input.
|
||||
// **`id`**`: string`
|
||||
// : The id to use for this input
|
||||
// **`attrs`**`: Object`
|
||||
// : The attributes to add to the input element.
|
||||
// **`value`**`: function(state) -> string`
|
||||
// : The getter for the input value.
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const id = randHtmlId();
|
||||
const inputAttrs = Object.assign({type: "text", name: this.options.id, id: this.options.id}, this.options.attrs || {})
|
||||
const input = crel("input", inputAttrs);
|
||||
const label = crel("label", {for: id}, this.options.label);
|
||||
|
||||
const rowRap = crel("div", {class: prefix + '-dialog-form-row'}, label, input);
|
||||
|
||||
const update = (state) => {
|
||||
input.value = this.options.value(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: rowRap, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogInput;
|
53
resources/js/editor/menu/DialogRadioOptions.js
Normal file
53
resources/js/editor/menu/DialogRadioOptions.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, randHtmlId} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogRadioOptions {
|
||||
/**
|
||||
* Given inputOptions should be keyed by label, with values being values.
|
||||
* Values of empty string will be treated as null.
|
||||
* @param {Object} inputOptions
|
||||
* @param {{label: string, id: string, attrs?: Object, value: function(PmEditorState): string|null}} options
|
||||
*/
|
||||
constructor(inputOptions, options) {
|
||||
this.inputOptions = inputOptions;
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
|
||||
const inputs = [];
|
||||
const optionInputLabels = Object.keys(this.inputOptions).map(label => {
|
||||
const inputAttrs = Object.assign({
|
||||
type: "radio",
|
||||
name: this.options.id,
|
||||
value: this.inputOptions[label],
|
||||
class: prefix + '-dialog-radio-option',
|
||||
}, this.options.attrs || {});
|
||||
const input = crel("input", inputAttrs);
|
||||
inputs.push(input);
|
||||
return crel("label", input, label);
|
||||
});
|
||||
|
||||
const optionInputWrap = crel("div", {class: prefix + '-dialog-radio-option-wrap'}, optionInputLabels);
|
||||
|
||||
const label = crel("label", {}, this.options.label);
|
||||
const rowRap = crel("div", {class: prefix + '-dialog-form-row'}, label, optionInputWrap);
|
||||
|
||||
const update = (state) => {
|
||||
const value = this.options.value(state);
|
||||
for (const input of inputs) {
|
||||
input.checked = (input.value === value || (value === null && input.value === ""));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: rowRap, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogRadioOptions;
|
42
resources/js/editor/menu/DialogTextArea.js
Normal file
42
resources/js/editor/menu/DialogTextArea.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
import {prefix, randHtmlId} from "./menu-utils";
|
||||
import crel from "crelt";
|
||||
|
||||
class DialogTextArea {
|
||||
// :: (?Object)
|
||||
// The following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show for the input.
|
||||
// **`id`**`: string`
|
||||
// : The id to use for this input
|
||||
// **`attrs`**`: Object`
|
||||
// : The attributes to add to the input element.
|
||||
// **`value`**`: function(state) -> string`
|
||||
// : The getter for the input value.
|
||||
constructor(options) {
|
||||
this.options = options || {};
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const id = randHtmlId();
|
||||
const inputAttrs = Object.assign({type: "text", name: this.options.id, id: this.options.id}, this.options.attrs || {})
|
||||
const input = crel("textarea", inputAttrs);
|
||||
const label = this.options.label ? crel("label", {for: id}, this.options.label) : null;
|
||||
|
||||
const rowRap = crel("div", {class: prefix + '-dialog-textarea-wrap'}, label, input);
|
||||
|
||||
const update = (state) => {
|
||||
input.value = this.options.value(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: rowRap, update}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DialogTextArea;
|
86
resources/js/editor/menu/TableCreatorGrid.js
Normal file
86
resources/js/editor/menu/TableCreatorGrid.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
import crel from "crelt"
|
||||
import {prefix} from "./menu-utils";
|
||||
import {insertTable} from "../commands";
|
||||
|
||||
class TableCreatorGrid {
|
||||
|
||||
constructor() {
|
||||
this.size = 10;
|
||||
this.label = null;
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
|
||||
const gridItems = [];
|
||||
for (let y = 0; y < this.size; y++) {
|
||||
for (let x = 0; x < this.size; x++) {
|
||||
const elem = crel("div", {class: prefix + "-table-creator-grid-item"});
|
||||
gridItems.push(elem);
|
||||
elem.addEventListener('mouseenter', event => {
|
||||
this.updateGridItemActiveStatus(elem, gridItems);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const gridWrap = crel("div", {
|
||||
class: prefix + "-table-creator-grid",
|
||||
style: `grid-template-columns: repeat(${this.size}, 14px);`,
|
||||
}, gridItems);
|
||||
|
||||
gridWrap.addEventListener('mouseleave', event => {
|
||||
this.updateGridItemActiveStatus(null, gridItems);
|
||||
});
|
||||
gridWrap.addEventListener('click', event => {
|
||||
if (event.target.classList.contains(prefix + "-table-creator-grid-item")) {
|
||||
const {x, y} = this.getPositionOfGridItem(event.target, gridItems);
|
||||
insertTable(y + 1, x + 1, {
|
||||
style: 'width: 100%;',
|
||||
})(view.state, view.dispatch);
|
||||
}
|
||||
});
|
||||
|
||||
const gridLabel = crel("div", {class: prefix + "-table-creator-grid-label"});
|
||||
this.label = gridLabel;
|
||||
const wrap = crel("div", {class: prefix + "-table-creator-grid-container"}, [gridWrap, gridLabel]);
|
||||
|
||||
function update(state) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element|null} newTarget
|
||||
* @param {Element[]} gridItems
|
||||
*/
|
||||
updateGridItemActiveStatus(newTarget, gridItems) {
|
||||
const {x: xPos, y: yPos} = this.getPositionOfGridItem(newTarget, gridItems);
|
||||
|
||||
for (let y = 0; y < this.size; y++) {
|
||||
for (let x = 0; x < this.size; x++) {
|
||||
const active = x <= xPos && y <= yPos;
|
||||
const index = (y * this.size) + x;
|
||||
gridItems[index].classList.toggle(prefix + "-table-creator-grid-item-active", active);
|
||||
}
|
||||
}
|
||||
|
||||
this.label.textContent = (xPos + yPos < 0) ? '' : `${xPos + 1} x ${yPos + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} gridItem
|
||||
* @param {Element[]} gridItems
|
||||
* @return {{x: number, y: number}}
|
||||
*/
|
||||
getPositionOfGridItem(gridItem, gridItems) {
|
||||
const index = gridItems.indexOf(gridItem);
|
||||
const y = Math.floor(index / this.size);
|
||||
const x = index % this.size;
|
||||
return {x, y};
|
||||
}
|
||||
}
|
||||
|
||||
export default TableCreatorGrid;
|
162
resources/js/editor/menu/icons.js
Normal file
162
resources/js/editor/menu/icons.js
Normal file
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
// :: Object
|
||||
// A set of basic editor-related icons. Contains the properties
|
||||
// `join`, `lift`, `selectParentNode`, `undo`, `redo`, `strong`, `em`,
|
||||
// `code`, `link`, `bulletList`, `orderedList`, and `blockquote`, each
|
||||
// holding an object that can be used as the `icon` option to
|
||||
// `MenuItem`.
|
||||
export const icons = {
|
||||
undo: {
|
||||
width: 24, height: 24,
|
||||
path: "M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z"
|
||||
},
|
||||
redo: {
|
||||
width: 24, height: 24,
|
||||
path: "M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7l-3.6 3.6z"
|
||||
},
|
||||
strong: {
|
||||
width: 24, height: 24,
|
||||
path: "M15.6 10.79c.97-.67 1.65-1.77 1.65-2.79 0-2.26-1.75-4-4-4H7v14h7.04c2.09 0 3.71-1.7 3.71-3.79 0-1.52-.86-2.82-2.15-3.42zM10 6.5h3c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-3v-3zm3.5 9H10v-3h3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5z"
|
||||
},
|
||||
em: {
|
||||
width: 24, height: 24,
|
||||
path: "M10 4v3h2.21l-3.42 8H6v3h8v-3h-2.21l3.42-8H18V4z"
|
||||
},
|
||||
link: {
|
||||
width: 24, height: 24,
|
||||
path: "M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"
|
||||
},
|
||||
bullet_list: {
|
||||
width: 24, height: 24,
|
||||
path: "M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.68-1.5 1.5s.68 1.5 1.5 1.5 1.5-.68 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"
|
||||
},
|
||||
ordered_list: {
|
||||
width: 24, height: 24,
|
||||
path: "M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"
|
||||
},
|
||||
task_list: {
|
||||
width: 24, height: 24,
|
||||
path: "M22,7h-9v2h9V7z M22,15h-9v2h9V15z M5.54,11L2,7.46l1.41-1.41l2.12,2.12l4.24-4.24l1.41,1.41L5.54,11z M5.54,19L2,15.46 l1.41-1.41l2.12,2.12l4.24-4.24l1.41,1.41L5.54,19z"
|
||||
},
|
||||
underline: {
|
||||
width: 24, height: 24,
|
||||
path: "M12 17c3.31 0 6-2.69 6-6V3h-2.5v8c0 1.93-1.57 3.5-3.5 3.5S8.5 12.93 8.5 11V3H6v8c0 3.31 2.69 6 6 6zm-7 2v2h14v-2H5z"
|
||||
},
|
||||
strike: {
|
||||
width: 24, height: 24,
|
||||
path: "M10 19h4v-3h-4v3zM5 4v3h5v3h4V7h5V4H5zM3 14h18v-2H3v2z"
|
||||
},
|
||||
superscript: {
|
||||
width: 24, height: 24,
|
||||
path: "M22,7h-2v1h3v1h-4V7c0-0.55,0.45-1,1-1h2V5h-3V4h3c0.55,0,1,0.45,1,1v1C23,6.55,22.55,7,22,7z M5.88,20h2.66l3.4-5.42h0.12 l3.4,5.42h2.66l-4.65-7.27L17.81,6h-2.68l-3.07,4.99h-0.12L8.85,6H6.19l4.32,6.73L5.88,20z"
|
||||
},
|
||||
subscript: {
|
||||
width: 24, height: 24,
|
||||
path: "M22,18h-2v1h3v1h-4v-2c0-0.55,0.45-1,1-1h2v-1h-3v-1h3c0.55,0,1,0.45,1,1v1C23,17.55,22.55,18,22,18z M5.88,18h2.66 l3.4-5.42h0.12l3.4,5.42h2.66l-4.65-7.27L17.81,4h-2.68l-3.07,4.99h-0.12L8.85,4H6.19l4.32,6.73L5.88,18z"
|
||||
},
|
||||
text_color: {
|
||||
width: 24, height: 24,
|
||||
path: "M2,20h20v4H2V20z M5.49,17h2.42l1.27-3.58h5.65L16.09,17h2.42L13.25,3h-2.5L5.49,17z M9.91,11.39l2.03-5.79h0.12l2.03,5.79 H9.91z"
|
||||
},
|
||||
background_color: {
|
||||
width: 24, height: 24,
|
||||
path: "M16.56,8.94L7.62,0L6.21,1.41l2.38,2.38L3.44,8.94c-0.59,0.59-0.59,1.54,0,2.12l5.5,5.5C9.23,16.85,9.62,17,10,17 s0.77-0.15,1.06-0.44l5.5-5.5C17.15,10.48,17.15,9.53,16.56,8.94z M5.21,10L10,5.21L14.79,10H5.21z M19,11.5c0,0-2,2.17-2,3.5 c0,1.1,0.9,2,2,2s2-0.9,2-2C21,13.67,19,11.5,19,11.5z M2,20h20v4H2V20z"
|
||||
},
|
||||
align_left: {
|
||||
width: 24, height: 24,
|
||||
path: "M15 15H3v2h12v-2zm0-8H3v2h12V7zM3 13h18v-2H3v2zm0 8h18v-2H3v2zM3 3v2h18V3H3z"
|
||||
},
|
||||
align_right: {
|
||||
width: 24, height: 24,
|
||||
path: "M3 21h18v-2H3v2zm6-4h12v-2H9v2zm-6-4h18v-2H3v2zm6-4h12V7H9v2zM3 3v2h18V3H3z"
|
||||
},
|
||||
align_center: {
|
||||
width: 24, height: 24,
|
||||
path: "M7 15v2h10v-2H7zm-4 6h18v-2H3v2zm0-8h18v-2H3v2zm4-6v2h10V7H7zM3 3v2h18V3H3z"
|
||||
},
|
||||
align_justify: {
|
||||
width: 24, height: 24,
|
||||
path: "M3 21h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18v-2H3v2zm0-4h18V7H3v2zm0-6v2h18V3H3z"
|
||||
},
|
||||
horizontal_rule: {
|
||||
width: 24, height: 24,
|
||||
path: "m 4,11 h 16 v 2 H 4 Z"
|
||||
},
|
||||
format_clear: {
|
||||
width: 24, height: 24,
|
||||
path: "M3.27 5L2 6.27l6.97 6.97L6.5 19h3l1.57-3.66L16.73 21 18 19.73 3.55 5.27 3.27 5zM6 5v.18L8.82 8h2.4l-.72 1.68 2.1 2.1L14.21 8H20V5H6z"
|
||||
},
|
||||
close: {
|
||||
width: 24, height: 24,
|
||||
path: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
||||
},
|
||||
source_code: {
|
||||
width: 24, height: 24,
|
||||
path: "M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z",
|
||||
},
|
||||
table: {
|
||||
width: 24, height: 24,
|
||||
path: "M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z",
|
||||
},
|
||||
iframe: {
|
||||
width: 24, height: 24,
|
||||
path: "m 22.71,18.43 c 0.03,-0.29 0.04,-0.58 0.01,-0.86 l 1.07,-0.85 c 0.1,-0.08 0.12,-0.21 0.06,-0.32 L 22.82,14.61 C 22.76,14.5 22.63,14.46 22.51,14.5 L 21.23,15 C 21,14.83 20.75,14.69 20.48,14.58 l -0.2,-1.36 C 20.26,13.09 20.16,13 20.03,13 h -2.07 c -0.12,0 -0.23,0.09 -0.25,0.21 l -0.2,1.36 c -0.26,0.11 -0.51,0.26 -0.74,0.42 l -1.28,-0.5 c -0.12,-0.05 -0.25,0 -0.31,0.11 l -1.03,1.79 c -0.06,0.11 -0.04,0.24 0.06,0.32 l 1.07,0.86 c -0.03,0.29 -0.04,0.58 -0.01,0.86 l -1.07,0.85 c -0.1,0.08 -0.12,0.21 -0.06,0.32 l 1.03,1.79 c 0.06,0.11 0.19,0.15 0.31,0.11 L 16.75,21 c 0.23,0.17 0.48,0.31 0.75,0.42 l 0.2,1.36 c 0.02,0.12 0.12,0.21 0.25,0.21 h 2.07 c 0.12,0 0.23,-0.09 0.25,-0.21 l 0.2,-1.36 c 0.26,-0.11 0.51,-0.26 0.74,-0.42 l 1.28,0.5 c 0.12,0.05 0.25,0 0.31,-0.11 l 1.03,-1.79 c 0.06,-0.11 0.04,-0.24 -0.06,-0.32 z M 19,19.5 c -0.83,0 -1.5,-0.67 -1.5,-1.5 0,-0.83 0.67,-1.5 1.5,-1.5 0.83,0 1.5,0.67 1.5,1.5 0,0.83 -0.67,1.5 -1.5,1.5 z M 15,12 9,8 v 8 z M 3,6 h 18 v 5 h 2 V 6 C 23,4.9 22.1,4 21,4 H 3 C 1.9,4 1,4.9 1,6 v 12 c 0,1.1 0.9,2 2,2 h 9 V 18 H 3 Z",
|
||||
},
|
||||
details: {
|
||||
width: 24, height: 24,
|
||||
path: "m 7,10 5,5 5,-5 z M 19,2.5 H 5 c -1.11,0 -2,0.9 -2,2 v 14 c 0,1.1 0.89,2 2,2 h 14 c 1.1,0 2,-0.9 2,-2 v -14 c 0,-1.1 -0.89,-2 -2,-2 z m 0,16 H 5 v -12 h 14 z",
|
||||
}
|
||||
};
|
||||
|
||||
const SVG = "http://www.w3.org/2000/svg"
|
||||
const XLINK = "http://www.w3.org/1999/xlink"
|
||||
|
||||
const prefix = "ProseMirror-icon"
|
||||
|
||||
function hashPath(path) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < path.length; i++)
|
||||
hash = (((hash << 5) - hash) + path.charCodeAt(i)) | 0
|
||||
return hash
|
||||
}
|
||||
|
||||
export function getIcon(icon) {
|
||||
let node = document.createElement("div")
|
||||
node.className = prefix
|
||||
if (icon.path) {
|
||||
let name = "pm-icon-" + hashPath(icon.path).toString(16)
|
||||
if (!document.getElementById(name)) buildSVG(name, icon)
|
||||
let svg = node.appendChild(document.createElementNS(SVG, "svg"))
|
||||
svg.style.width = (icon.width / icon.height) + "em"
|
||||
let use = svg.appendChild(document.createElementNS(SVG, "use"))
|
||||
use.setAttributeNS(XLINK, "href", /([^#]*)/.exec(document.location)[1] + "#" + name)
|
||||
} else if (icon.dom) {
|
||||
node.appendChild(icon.dom.cloneNode(true))
|
||||
} else {
|
||||
node.appendChild(document.createElement("span")).textContent = icon.text || ''
|
||||
if (icon.css) node.firstChild.style.cssText = icon.css
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
function buildSVG(name, data) {
|
||||
let collection = document.getElementById(prefix + "-collection")
|
||||
if (!collection) {
|
||||
collection = document.createElementNS(SVG, "svg")
|
||||
collection.id = prefix + "-collection"
|
||||
collection.style.display = "none"
|
||||
document.body.insertBefore(collection, document.body.firstChild)
|
||||
}
|
||||
let sym = document.createElementNS(SVG, "symbol")
|
||||
sym.id = name
|
||||
sym.setAttribute("viewBox", "0 0 " + data.width + " " + data.height)
|
||||
let path = sym.appendChild(document.createElementNS(SVG, "path"))
|
||||
path.setAttribute("d", data.path)
|
||||
collection.appendChild(sym)
|
||||
}
|
207
resources/js/editor/menu/index.js
Normal file
207
resources/js/editor/menu/index.js
Normal file
|
@ -0,0 +1,207 @@
|
|||
import {
|
||||
MenuItem, Dropdown, DropdownSubmenu, renderGrouped, joinUpItem, liftItem, selectParentNodeItem,
|
||||
undoItem, redoItem, wrapItem, blockTypeItem, setAttrItem, insertBlockBeforeItem,
|
||||
} from "./menu"
|
||||
import {icons} from "./icons";
|
||||
import ColorPickerGrid from "./ColorPickerGrid";
|
||||
import TableCreatorGrid from "./TableCreatorGrid";
|
||||
import {toggleMark} from "prosemirror-commands";
|
||||
import {menuBar} from "./menubar"
|
||||
import schema from "../schema";
|
||||
import {removeMarks} from "../commands";
|
||||
|
||||
import itemAnchorButtonItem from "./item-anchor-button";
|
||||
import itemHtmlSourceButton from "./item-html-source-button";
|
||||
import itemIframeButton from "./item-iframe-button";
|
||||
|
||||
|
||||
function cmdItem(cmd, options) {
|
||||
const passedOptions = {
|
||||
label: options.title,
|
||||
run: cmd
|
||||
};
|
||||
for (const prop in options) {
|
||||
passedOptions[prop] = options[prop];
|
||||
}
|
||||
if ((!options.enable || options.enable === true) && !options.select) {
|
||||
passedOptions[options.enable ? "enable" : "select"] = function (state) {
|
||||
return cmd(state);
|
||||
};
|
||||
}
|
||||
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
function markActive(state, type) {
|
||||
const ref = state.selection;
|
||||
const from = ref.from;
|
||||
const $from = ref.$from;
|
||||
const to = ref.to;
|
||||
const empty = ref.empty;
|
||||
if (empty) {
|
||||
return type.isInSet(state.storedMarks || $from.marks())
|
||||
} else {
|
||||
return state.doc.rangeHasMark(from, to, type)
|
||||
}
|
||||
}
|
||||
|
||||
function markItem(markType, options) {
|
||||
const passedOptions = {
|
||||
active: function active(state) {
|
||||
return markActive(state, markType)
|
||||
},
|
||||
enable: true
|
||||
};
|
||||
for (const prop in options) {
|
||||
passedOptions[prop] = options[prop];
|
||||
}
|
||||
|
||||
return cmdItem(toggleMark(markType, passedOptions.attrs), passedOptions)
|
||||
}
|
||||
|
||||
const inlineStyles = [
|
||||
markItem(schema.marks.strong, {title: "Bold", icon: icons.strong}),
|
||||
markItem(schema.marks.em, {title: "Italic", icon: icons.em}),
|
||||
markItem(schema.marks.underline, {title: "Underline", icon: icons.underline}),
|
||||
markItem(schema.marks.strike, {title: "Strikethrough", icon: icons.strike}),
|
||||
markItem(schema.marks.superscript, {title: "Superscript", icon: icons.superscript}),
|
||||
markItem(schema.marks.subscript, {title: "Subscript", icon: icons.subscript}),
|
||||
];
|
||||
|
||||
const formats = [
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Large",
|
||||
attrs: {level: 2}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Medium",
|
||||
attrs: {level: 3}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Small",
|
||||
attrs: {level: 4}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.heading, {
|
||||
label: "Header Tiny",
|
||||
attrs: {level: 5}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.paragraph, {
|
||||
label: "Paragraph",
|
||||
attrs: {}
|
||||
}),
|
||||
markItem(schema.marks.code, {
|
||||
label: "Inline Code",
|
||||
attrs: {}
|
||||
}),
|
||||
new DropdownSubmenu([
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Info Callout",
|
||||
attrs: {type: 'info'}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Danger Callout",
|
||||
attrs: {type: 'danger'}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Success Callout",
|
||||
attrs: {type: 'success'}
|
||||
}),
|
||||
blockTypeItem(schema.nodes.callout, {
|
||||
label: "Warning Callout",
|
||||
attrs: {type: 'warning'}
|
||||
})
|
||||
], { label: 'Callouts' }),
|
||||
];
|
||||
|
||||
const alignments = [
|
||||
setAttrItem('align', 'left', {
|
||||
icon: icons.align_left
|
||||
}),
|
||||
setAttrItem('align', 'center', {
|
||||
icon: icons.align_center
|
||||
}),
|
||||
setAttrItem('align', 'right', {
|
||||
icon: icons.align_right
|
||||
}),
|
||||
setAttrItem('align', 'justify', {
|
||||
icon: icons.align_justify
|
||||
}),
|
||||
];
|
||||
|
||||
const colorOptions = ["#000000","#993300","#333300","#003300","#003366","#000080","#333399","#333333","#800000","#FF6600","#808000","#008000","#008080","#0000FF","#666699","#808080","#FF0000","#FF9900","#99CC00","#339966","#33CCCC","#3366FF","#800080","#999999","#FF00FF","#FFCC00","#FFFF00","#00FF00","#00FFFF","#00CCFF","#993366","#FFFFFF","#FF99CC","#FFCC99","#FFFF99","#CCFFCC","#CCFFFF","#99CCFF","#CC99FF"];
|
||||
|
||||
const colors = [
|
||||
new DropdownSubmenu([
|
||||
new ColorPickerGrid(schema.marks.text_color, 'color', colorOptions),
|
||||
], {icon: icons.text_color}),
|
||||
new DropdownSubmenu([
|
||||
new ColorPickerGrid(schema.marks.background_color, 'color', colorOptions),
|
||||
], {icon: icons.background_color}),
|
||||
];
|
||||
|
||||
const lists = [
|
||||
wrapItem(schema.nodes.bullet_list, {
|
||||
title: "Bullet List",
|
||||
icon: icons.bullet_list,
|
||||
}),
|
||||
wrapItem(schema.nodes.ordered_list, {
|
||||
title: "Ordered List",
|
||||
icon: icons.ordered_list,
|
||||
}),
|
||||
];
|
||||
|
||||
const inserts = [
|
||||
itemAnchorButtonItem(),
|
||||
insertBlockBeforeItem(schema.nodes.horizontal_rule, {
|
||||
title: "Horizontal Rule",
|
||||
icon: icons.horizontal_rule,
|
||||
}),
|
||||
new DropdownSubmenu([
|
||||
new TableCreatorGrid()
|
||||
], {icon: icons.table}),
|
||||
itemIframeButton(),
|
||||
wrapItem(schema.nodes.details, {
|
||||
title: "Dropdown Block",
|
||||
icon: icons.details,
|
||||
})
|
||||
];
|
||||
|
||||
const utilities = [
|
||||
new MenuItem({
|
||||
title: 'Clear Formatting',
|
||||
icon: icons.format_clear,
|
||||
run: removeMarks(),
|
||||
enable: state => true,
|
||||
}),
|
||||
itemHtmlSourceButton(),
|
||||
];
|
||||
|
||||
const menu = menuBar({
|
||||
floating: false,
|
||||
content: [
|
||||
[undoItem, redoItem],
|
||||
[new DropdownSubmenu(formats, { label: 'Formats' })],
|
||||
inlineStyles,
|
||||
colors,
|
||||
alignments,
|
||||
lists,
|
||||
inserts,
|
||||
utilities,
|
||||
],
|
||||
});
|
||||
|
||||
export default menu;
|
||||
|
||||
// !! This module defines a number of building blocks for ProseMirror
|
||||
// menus, along with a [menu bar](#menu.menuBar) implementation.
|
||||
|
||||
// MenuElement:: interface
|
||||
// The types defined in this module aren't the only thing you can
|
||||
// display in your menu. Anything that conforms to this interface can
|
||||
// be put into a menu structure.
|
||||
//
|
||||
// render:: (pm: EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Render the element for display in the menu. Must return a DOM
|
||||
// element and a function that can be used to update the element to
|
||||
// a new state. The `update` function will return false if the
|
||||
// update hid the entire element.
|
120
resources/js/editor/menu/item-anchor-button.js
Normal file
120
resources/js/editor/menu/item-anchor-button.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
import DialogBox from "./DialogBox";
|
||||
import DialogForm from "./DialogForm";
|
||||
import DialogInput from "./DialogInput";
|
||||
import DialogRadioOptions from "./DialogRadioOptions";
|
||||
import schema from "../schema";
|
||||
|
||||
import {MenuItem} from "./menu";
|
||||
import {icons} from "./icons";
|
||||
import {expandSelectionToMark, nullifyEmptyValues} from "../util";
|
||||
|
||||
/**
|
||||
* @param {PmMarkType} markType
|
||||
* @param {String} attribute
|
||||
* @return {(function(PmEditorState): (string|null))}
|
||||
*/
|
||||
function getMarkAttribute(markType, attribute) {
|
||||
return function (state) {
|
||||
const marks = state.selection.$head.marks();
|
||||
for (const mark of marks) {
|
||||
if (mark.type === markType) {
|
||||
return mark.attrs[attribute];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(function(FormData))} submitter
|
||||
* @param {Function} closer
|
||||
* @return {DialogBox}
|
||||
*/
|
||||
function getLinkDialog(submitter, closer) {
|
||||
return new DialogBox([
|
||||
new DialogForm([
|
||||
new DialogInput({
|
||||
label: 'URL',
|
||||
id: 'href',
|
||||
value: getMarkAttribute(schema.marks.link, 'href'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Hover Label',
|
||||
id: 'title',
|
||||
value: getMarkAttribute(schema.marks.link, 'title'),
|
||||
}),
|
||||
new DialogRadioOptions({
|
||||
"Same tab or window": "",
|
||||
"New tab or window": "_blank",
|
||||
}, {
|
||||
label: 'Behaviour',
|
||||
id: 'target',
|
||||
value: getMarkAttribute(schema.marks.link, 'target'),
|
||||
})
|
||||
], {
|
||||
canceler: closer,
|
||||
action: submitter,
|
||||
}),
|
||||
], {
|
||||
label: 'Insert Link',
|
||||
closer: closer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} formData
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @return {boolean}
|
||||
*/
|
||||
function applyLink(formData, state, dispatch) {
|
||||
const selection = state.selection;
|
||||
const attrs = nullifyEmptyValues(Object.fromEntries(formData));
|
||||
if (!dispatch) return true;
|
||||
|
||||
const tr = state.tr;
|
||||
const {from, to} = expandSelectionToMark(state, selection, schema.marks.link);
|
||||
|
||||
if (attrs.href) {
|
||||
tr.addMark(from, to, schema.marks.link.create(attrs));
|
||||
} else {
|
||||
tr.removeMark(from, to, schema.marks.link);
|
||||
}
|
||||
|
||||
dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @param {PmView} view
|
||||
* @param {Event} e
|
||||
*/
|
||||
function onPress(state, dispatch, view, e) {
|
||||
const dialog = getLinkDialog((data) => {
|
||||
applyLink(data, state, dispatch);
|
||||
dom.remove();
|
||||
}, () => {
|
||||
dom.remove();
|
||||
})
|
||||
|
||||
const {dom, update} = dialog.render(view);
|
||||
update(state);
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {MenuItem}
|
||||
*/
|
||||
function anchorButtonItem() {
|
||||
return new MenuItem({
|
||||
title: "Insert/Edit Anchor Link",
|
||||
run: onPress,
|
||||
enable: state => true,
|
||||
icon: icons.link,
|
||||
});
|
||||
}
|
||||
|
||||
export default anchorButtonItem;
|
87
resources/js/editor/menu/item-html-source-button.js
Normal file
87
resources/js/editor/menu/item-html-source-button.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import DialogBox from "./DialogBox";
|
||||
import DialogForm from "./DialogForm";
|
||||
import DialogTextArea from "./DialogTextArea";
|
||||
|
||||
import {MenuItem} from "./menu";
|
||||
import {icons} from "./icons";
|
||||
import {htmlToDoc, stateToHtml} from "../util";
|
||||
|
||||
/**
|
||||
* @param {(function(FormData))} submitter
|
||||
* @param {Function} closer
|
||||
* @return {DialogBox}
|
||||
*/
|
||||
function getLinkDialog(submitter, closer) {
|
||||
return new DialogBox([
|
||||
new DialogForm([
|
||||
new DialogTextArea({
|
||||
id: 'source',
|
||||
value: stateToHtml,
|
||||
attrs: {
|
||||
rows: 10,
|
||||
cols: 50,
|
||||
}
|
||||
}),
|
||||
], {
|
||||
canceler: closer,
|
||||
action: submitter,
|
||||
}),
|
||||
], {
|
||||
label: 'View/Edit HTML Source',
|
||||
closer: closer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} formData
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @return {boolean}
|
||||
*/
|
||||
function replaceEditorHtml(formData, state, dispatch) {
|
||||
const html = formData.get('source');
|
||||
|
||||
if (dispatch) {
|
||||
const tr = state.tr;
|
||||
|
||||
const newDoc = htmlToDoc(html);
|
||||
tr.replaceWith(0, state.doc.content.size, newDoc.content);
|
||||
dispatch(tr);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @param {PmView} view
|
||||
* @param {Event} e
|
||||
*/
|
||||
function onPress(state, dispatch, view, e) {
|
||||
const dialog = getLinkDialog((data) => {
|
||||
replaceEditorHtml(data, state, dispatch);
|
||||
dom.remove();
|
||||
}, () => {
|
||||
dom.remove();
|
||||
})
|
||||
|
||||
const {dom, update} = dialog.render(view);
|
||||
update(state);
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {MenuItem}
|
||||
*/
|
||||
function htmlSourceButtonItem() {
|
||||
return new MenuItem({
|
||||
title: "View HTML Source",
|
||||
run: onPress,
|
||||
enable: state => true,
|
||||
icon: icons.source_code,
|
||||
});
|
||||
}
|
||||
|
||||
export default htmlSourceButtonItem;
|
115
resources/js/editor/menu/item-iframe-button.js
Normal file
115
resources/js/editor/menu/item-iframe-button.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
import DialogBox from "./DialogBox";
|
||||
import DialogForm from "./DialogForm";
|
||||
import DialogInput from "./DialogInput";
|
||||
import schema from "../schema";
|
||||
|
||||
import {MenuItem} from "./menu";
|
||||
import {icons} from "./icons";
|
||||
import {nullifyEmptyValues} from "../util";
|
||||
|
||||
/**
|
||||
* @param {PmNodeType} nodeType
|
||||
* @param {String} attribute
|
||||
* @return {(function(PmEditorState): (string|null))}
|
||||
*/
|
||||
function getNodeAttribute(nodeType, attribute) {
|
||||
return function (state) {
|
||||
const node = state.selection.node;
|
||||
if (node && node.type === nodeType) {
|
||||
return node.attrs[attribute];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(function(FormData))} submitter
|
||||
* @param {Function} closer
|
||||
* @return {DialogBox}
|
||||
*/
|
||||
function getLinkDialog(submitter, closer) {
|
||||
return new DialogBox([
|
||||
new DialogForm([
|
||||
new DialogInput({
|
||||
label: 'Source URL',
|
||||
id: 'src',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'src'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Hover Label',
|
||||
id: 'title',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'title'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Width',
|
||||
id: 'width',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'width'),
|
||||
}),
|
||||
new DialogInput({
|
||||
label: 'Height',
|
||||
id: 'height',
|
||||
value: getNodeAttribute(schema.nodes.iframe, 'height'),
|
||||
}),
|
||||
], {
|
||||
canceler: closer,
|
||||
action: submitter,
|
||||
}),
|
||||
], {
|
||||
label: 'Insert Embedded Content',
|
||||
closer: closer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FormData} formData
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @return {boolean}
|
||||
*/
|
||||
function applyIframe(formData, state, dispatch) {
|
||||
const attrs = nullifyEmptyValues(Object.fromEntries(formData));
|
||||
if (!dispatch) return true;
|
||||
|
||||
const tr = state.tr;
|
||||
const currentNodeAttrs = state.selection?.nodes?.attrs || {};
|
||||
const newAttrs = Object.assign({}, currentNodeAttrs, attrs);
|
||||
tr.replaceSelectionWith(schema.nodes.iframe.create(newAttrs));
|
||||
|
||||
dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
* @param {PmView} view
|
||||
* @param {Event} e
|
||||
*/
|
||||
function onPress(state, dispatch, view, e) {
|
||||
const dialog = getLinkDialog((data) => {
|
||||
applyIframe(data, state, dispatch);
|
||||
dom.remove();
|
||||
}, () => {
|
||||
dom.remove();
|
||||
})
|
||||
|
||||
const {dom, update} = dialog.render(view);
|
||||
update(state);
|
||||
document.body.appendChild(dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {MenuItem}
|
||||
*/
|
||||
function iframeButtonItem() {
|
||||
return new MenuItem({
|
||||
title: "Embed Content",
|
||||
run: onPress,
|
||||
enable: state => true,
|
||||
active: state => (state.selection.node || {type: ''}).type === schema.nodes.iframe,
|
||||
icon: icons.iframe,
|
||||
});
|
||||
}
|
||||
|
||||
export default iframeButtonItem;
|
39
resources/js/editor/menu/menu-utils.js
Normal file
39
resources/js/editor/menu/menu-utils.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import crel from "crelt";
|
||||
|
||||
export const prefix = "ProseMirror-menu";
|
||||
|
||||
export function renderDropdownItems(items, view) {
|
||||
let rendered = [], updates = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let {dom, update} = items[i].render(view)
|
||||
rendered.push(crel("div", {class: prefix + "-dropdown-item"}, dom))
|
||||
updates.push(update)
|
||||
}
|
||||
return {dom: rendered, update: combineUpdates(updates, rendered)}
|
||||
}
|
||||
|
||||
export function renderItems(items, view) {
|
||||
let rendered = [], updates = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
let {dom, update} = items[i].render(view)
|
||||
rendered.push(dom);
|
||||
updates.push(update)
|
||||
}
|
||||
return {dom: rendered, update: combineUpdates(updates, rendered)}
|
||||
}
|
||||
|
||||
export function combineUpdates(updates, nodes) {
|
||||
return state => {
|
||||
let something = false
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
let up = updates[i](state)
|
||||
nodes[i].style.display = up ? "" : "none"
|
||||
if (up) something = true
|
||||
}
|
||||
return something
|
||||
}
|
||||
}
|
||||
|
||||
export function randHtmlId() {
|
||||
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 9);
|
||||
}
|
419
resources/js/editor/menu/menu.js
Normal file
419
resources/js/editor/menu/menu.js
Normal file
|
@ -0,0 +1,419 @@
|
|||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
import crel from "crelt"
|
||||
import {lift, joinUp, selectParentNode, wrapIn, setBlockType, toggleMark} from "prosemirror-commands"
|
||||
import {undo, redo} from "prosemirror-history"
|
||||
import {setBlockAttr, insertBlockBefore} from "../commands";
|
||||
import {renderDropdownItems, combineUpdates} from "./menu-utils";
|
||||
|
||||
import {getIcon, icons} from "./icons"
|
||||
import {prefix} from "./menu-utils";
|
||||
|
||||
// ::- An icon or label that, when clicked, executes a command.
|
||||
export class MenuItem {
|
||||
// :: (MenuItemSpec)
|
||||
constructor(spec) {
|
||||
// :: MenuItemSpec
|
||||
// The spec used to create the menu item.
|
||||
this.spec = spec
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the icon according to its [display
|
||||
// spec](#menu.MenuItemSpec.display), and adds an event handler which
|
||||
// executes the command when the representation is clicked.
|
||||
render(view) {
|
||||
let spec = this.spec
|
||||
let dom = spec.render ? spec.render(view)
|
||||
: spec.icon ? getIcon(spec.icon)
|
||||
: spec.label ? crel("div", null, translate(view, spec.label))
|
||||
: null
|
||||
if (!dom) throw new RangeError("MenuItem without icon or label property")
|
||||
if (spec.title) {
|
||||
const title = (typeof spec.title === "function" ? spec.title(view.state) : spec.title)
|
||||
dom.setAttribute("title", translate(view, title))
|
||||
}
|
||||
if (spec.class) dom.classList.add(spec.class)
|
||||
if (spec.css) dom.style.cssText += spec.css
|
||||
|
||||
dom.addEventListener("mousedown", e => {
|
||||
e.preventDefault()
|
||||
if (!dom.classList.contains(prefix + "-disabled"))
|
||||
spec.run(view.state, view.dispatch, view, e)
|
||||
})
|
||||
|
||||
function update(state) {
|
||||
if (spec.select) {
|
||||
let selected = spec.select(state)
|
||||
dom.style.display = selected ? "" : "none"
|
||||
if (!selected) return false
|
||||
}
|
||||
let enabled = true
|
||||
if (spec.enable) {
|
||||
enabled = spec.enable(state) || false
|
||||
setClass(dom, prefix + "-disabled", !enabled)
|
||||
}
|
||||
if (spec.active) {
|
||||
let active = enabled && spec.active(state) || false
|
||||
setClass(dom, prefix + "-active", active)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return {dom, update}
|
||||
}
|
||||
}
|
||||
|
||||
function translate(view, text) {
|
||||
return view._props.translate ? view._props.translate(text) : text
|
||||
}
|
||||
|
||||
// MenuItemSpec:: interface
|
||||
// The configuration object passed to the `MenuItem` constructor.
|
||||
//
|
||||
// run:: (EditorState, (Transaction), EditorView, dom.Event)
|
||||
// The function to execute when the menu item is activated.
|
||||
//
|
||||
// select:: ?(EditorState) → bool
|
||||
// Optional function that is used to determine whether the item is
|
||||
// appropriate at the moment. Deselected items will be hidden.
|
||||
//
|
||||
// enable:: ?(EditorState) → bool
|
||||
// Function that is used to determine if the item is enabled. If
|
||||
// given and returning false, the item will be given a disabled
|
||||
// styling.
|
||||
//
|
||||
// active:: ?(EditorState) → bool
|
||||
// A predicate function to determine whether the item is 'active' (for
|
||||
// example, the item for toggling the strong mark might be active then
|
||||
// the cursor is in strong text).
|
||||
//
|
||||
// render:: ?(EditorView) → dom.Node
|
||||
// A function that renders the item. You must provide either this,
|
||||
// [`icon`](#menu.MenuItemSpec.icon), or [`label`](#MenuItemSpec.label).
|
||||
//
|
||||
// icon:: ?Object
|
||||
// Describes an icon to show for this item. The object may specify
|
||||
// an SVG icon, in which case its `path` property should be an [SVG
|
||||
// path
|
||||
// spec](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d),
|
||||
// and `width` and `height` should provide the viewbox in which that
|
||||
// path exists. Alternatively, it may have a `text` property
|
||||
// specifying a string of text that makes up the icon, with an
|
||||
// optional `css` property giving additional CSS styling for the
|
||||
// text. _Or_ it may contain `dom` property containing a DOM node.
|
||||
//
|
||||
// label:: ?string
|
||||
// Makes the item show up as a text label. Mostly useful for items
|
||||
// wrapped in a [drop-down](#menu.Dropdown) or similar menu. The object
|
||||
// should have a `label` property providing the text to display.
|
||||
//
|
||||
// title:: ?union<string, (EditorState) → string>
|
||||
// Defines DOM title (mouseover) text for the item.
|
||||
//
|
||||
// class:: ?string
|
||||
// Optionally adds a CSS class to the item's DOM representation.
|
||||
//
|
||||
// css:: ?string
|
||||
// Optionally adds a string of inline CSS to the item's DOM
|
||||
// representation.
|
||||
|
||||
let lastMenuEvent = {time: 0, node: null}
|
||||
function markMenuEvent(e) {
|
||||
lastMenuEvent.time = Date.now()
|
||||
lastMenuEvent.node = e.target
|
||||
}
|
||||
function isMenuEvent(wrapper) {
|
||||
return Date.now() - 100 < lastMenuEvent.time &&
|
||||
lastMenuEvent.node && wrapper.contains(lastMenuEvent.node)
|
||||
}
|
||||
|
||||
// ::- A drop-down menu, displayed as a label with a downwards-pointing
|
||||
// triangle to the right of it.
|
||||
export class Dropdown {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// Create a dropdown wrapping the elements. Options may include
|
||||
// the following properties:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show on the drop-down control.
|
||||
//
|
||||
// **`title`**`: string`
|
||||
// : Sets the
|
||||
// [`title`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/title)
|
||||
// attribute given to the menu control.
|
||||
//
|
||||
// **`class`**`: string`
|
||||
// : When given, adds an extra CSS class to the menu control.
|
||||
//
|
||||
// **`css`**`: string`
|
||||
// : When given, adds an extra set of CSS styles to the menu control.
|
||||
constructor(content, options) {
|
||||
this.options = options || {}
|
||||
this.content = Array.isArray(content) ? content : [content]
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState)}
|
||||
// Render the dropdown menu and sub-items.
|
||||
render(view) {
|
||||
let content = renderDropdownItems(this.content, view)
|
||||
|
||||
let label = crel("div", {class: prefix + "-dropdown " + (this.options.class || ""),
|
||||
style: this.options.css},
|
||||
translate(view, this.options.label))
|
||||
if (this.options.title) label.setAttribute("title", translate(view, this.options.title))
|
||||
let wrap = crel("div", {class: prefix + "-dropdown-wrap"}, label)
|
||||
let open = null, listeningOnClose = null
|
||||
let close = () => {
|
||||
if (open && open.close()) {
|
||||
open = null
|
||||
window.removeEventListener("mousedown", listeningOnClose)
|
||||
}
|
||||
}
|
||||
label.addEventListener("mousedown", e => {
|
||||
e.preventDefault()
|
||||
markMenuEvent(e)
|
||||
if (open) {
|
||||
close()
|
||||
} else {
|
||||
open = this.expand(wrap, content.dom)
|
||||
window.addEventListener("mousedown", listeningOnClose = () => {
|
||||
if (!isMenuEvent(wrap)) close()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
function update(state) {
|
||||
let inner = content.update(state)
|
||||
wrap.style.display = inner ? "" : "none"
|
||||
return inner
|
||||
}
|
||||
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
|
||||
expand(dom, items) {
|
||||
let menuDOM = crel("div", {class: prefix + "-dropdown-menu " + (this.options.class || "")}, items)
|
||||
|
||||
let done = false
|
||||
function close() {
|
||||
if (done) return
|
||||
done = true
|
||||
dom.removeChild(menuDOM)
|
||||
return true
|
||||
}
|
||||
dom.appendChild(menuDOM)
|
||||
return {close, node: menuDOM}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ::- Represents a submenu wrapping a group of elements that start
|
||||
// hidden and expand to the right when hovered over or tapped.
|
||||
export class DropdownSubmenu {
|
||||
// :: ([MenuElement], ?Object)
|
||||
// Creates a submenu for the given group of menu elements. The
|
||||
// following options are recognized:
|
||||
//
|
||||
// **`label`**`: string`
|
||||
// : The label to show on the submenu.
|
||||
constructor(content, options) {
|
||||
this.options = options || {}
|
||||
this.content = Array.isArray(content) ? content : [content]
|
||||
}
|
||||
|
||||
// :: (EditorView) → {dom: dom.Node, update: (EditorState) → bool}
|
||||
// Renders the submenu.
|
||||
render(view) {
|
||||
const items = renderDropdownItems(this.content, view)
|
||||
|
||||
const handleContent = this.options.icon ? getIcon(this.options.icon) : crel("div", {class: prefix + "-submenu-label"}, translate(view, this.options.label));
|
||||
const wrap = crel("div", {class: prefix + "-submenu-wrap"}, handleContent,
|
||||
crel("div", {class: prefix + "-submenu"}, items.dom))
|
||||
let listeningOnClose = null
|
||||
handleContent.addEventListener("mousedown", e => {
|
||||
e.preventDefault()
|
||||
markMenuEvent(e)
|
||||
setClass(wrap, prefix + "-submenu-wrap-active")
|
||||
if (!listeningOnClose)
|
||||
window.addEventListener("mousedown", listeningOnClose = () => {
|
||||
if (!isMenuEvent(wrap)) {
|
||||
wrap.classList.remove(prefix + "-submenu-wrap-active")
|
||||
window.removeEventListener("mousedown", listeningOnClose)
|
||||
listeningOnClose = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function update(state) {
|
||||
let inner = items.update(state)
|
||||
wrap.style.display = inner ? "" : "none"
|
||||
return inner
|
||||
}
|
||||
return {dom: wrap, update}
|
||||
}
|
||||
}
|
||||
|
||||
// :: (EditorView, [[MenuElement]]) → {dom: dom.DocumentFragment, update: (EditorState) → bool}
|
||||
// Render the given, possibly nested, array of menu elements into a
|
||||
// document fragment, placing separators between them (and ensuring no
|
||||
// superfluous separators appear when some of the groups turn out to
|
||||
// be empty).
|
||||
export function renderGrouped(view, content) {
|
||||
let result = document.createDocumentFragment()
|
||||
let updates = [], separators = []
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let items = content[i], localUpdates = [], localNodes = []
|
||||
for (let j = 0; j < items.length; j++) {
|
||||
let {dom, update} = items[j].render(view)
|
||||
let span = crel("span", {class: prefix + "item"}, dom)
|
||||
result.appendChild(span)
|
||||
localNodes.push(span)
|
||||
localUpdates.push(update)
|
||||
}
|
||||
if (localUpdates.length) {
|
||||
updates.push(combineUpdates(localUpdates, localNodes))
|
||||
if (i < content.length - 1)
|
||||
separators.push(result.appendChild(separator()))
|
||||
}
|
||||
}
|
||||
|
||||
function update(state) {
|
||||
let something = false, needSep = false
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
let hasContent = updates[i](state)
|
||||
if (i) separators[i - 1].style.display = needSep && hasContent ? "" : "none"
|
||||
needSep = hasContent
|
||||
if (hasContent) something = true
|
||||
}
|
||||
return something
|
||||
}
|
||||
return {dom: result, update}
|
||||
}
|
||||
|
||||
function separator() {
|
||||
return crel("span", {class: prefix + "separator"})
|
||||
}
|
||||
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `joinUp` command.
|
||||
export const joinUpItem = new MenuItem({
|
||||
title: "Join with above block",
|
||||
run: joinUp,
|
||||
select: state => joinUp(state),
|
||||
icon: icons.join
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `lift` command.
|
||||
export const liftItem = new MenuItem({
|
||||
title: "Lift out of enclosing block",
|
||||
run: lift,
|
||||
select: state => lift(state),
|
||||
icon: icons.lift
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `selectParentNode` command.
|
||||
export const selectParentNodeItem = new MenuItem({
|
||||
title: "Select parent node",
|
||||
run: selectParentNode,
|
||||
select: state => selectParentNode(state),
|
||||
icon: icons.selectParentNode
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `undo` command.
|
||||
export let undoItem = new MenuItem({
|
||||
title: "Undo last change",
|
||||
run: undo,
|
||||
enable: state => undo(state),
|
||||
icon: icons.undo
|
||||
})
|
||||
|
||||
// :: MenuItem
|
||||
// Menu item for the `redo` command.
|
||||
export let redoItem = new MenuItem({
|
||||
title: "Redo last undone change",
|
||||
run: redo,
|
||||
enable: state => redo(state),
|
||||
icon: icons.redo
|
||||
})
|
||||
|
||||
// :: (NodeType, Object) → MenuItem
|
||||
// Build a menu item for wrapping the selection in a given node type.
|
||||
// Adds `run` and `select` properties to the ones present in
|
||||
// `options`. `options.attrs` may be an object or a function.
|
||||
export function wrapItem(nodeType, options) {
|
||||
let passedOptions = {
|
||||
run(state, dispatch) {
|
||||
// FIXME if (options.attrs instanceof Function) options.attrs(state, attrs => wrapIn(nodeType, attrs)(state))
|
||||
return wrapIn(nodeType, options.attrs)(state, dispatch)
|
||||
},
|
||||
select(state) {
|
||||
return wrapIn(nodeType, options.attrs instanceof Function ? null : options.attrs)(state)
|
||||
}
|
||||
}
|
||||
for (let prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
// :: (NodeType, Object) → MenuItem
|
||||
// Build a menu item for changing the type of the textblock around the
|
||||
// selection to the given type. Provides `run`, `active`, and `select`
|
||||
// properties. Others must be given in `options`. `options.attrs` may
|
||||
// be an object to provide the attributes for the textblock node.
|
||||
export function blockTypeItem(nodeType, options) {
|
||||
let command = setBlockType(nodeType, options.attrs)
|
||||
let passedOptions = {
|
||||
run: command,
|
||||
enable(state) { return command(state) },
|
||||
active(state) {
|
||||
let {$from, to, node} = state.selection
|
||||
if (node) return node.hasMarkup(nodeType, options.attrs)
|
||||
return to <= $from.end() && $from.parent.hasMarkup(nodeType, options.attrs)
|
||||
}
|
||||
}
|
||||
for (let prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
export function setAttrItem(attrName, attrValue, options) {
|
||||
const command = setBlockAttr(attrName, attrValue);
|
||||
const passedOptions = {
|
||||
run: command,
|
||||
enable(state) { return command(state) },
|
||||
active(state) {
|
||||
const {$from, to, node} = state.selection
|
||||
if (node) return node.attrs[attrValue] === attrValue;
|
||||
return to <= $from.end() && $from.parent.attrs[attrValue] === attrValue;
|
||||
}
|
||||
}
|
||||
for (const prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions)
|
||||
}
|
||||
|
||||
export function insertBlockBeforeItem(blockType, options) {
|
||||
const command = insertBlockBefore(blockType);
|
||||
const passedOptions = {
|
||||
run: command,
|
||||
enable(state) { return command(state) },
|
||||
active(state) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const prop in options) passedOptions[prop] = options[prop]
|
||||
return new MenuItem(passedOptions);
|
||||
}
|
||||
|
||||
// Work around classList.toggle being broken in IE11
|
||||
function setClass(dom, cls, on) {
|
||||
if (on) dom.classList.add(cls)
|
||||
else dom.classList.remove(cls)
|
||||
}
|
163
resources/js/editor/menu/menubar.js
Normal file
163
resources/js/editor/menu/menubar.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-menu
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
import crel from "crelt"
|
||||
import {Plugin} from "prosemirror-state"
|
||||
|
||||
import {renderGrouped} from "./menu"
|
||||
|
||||
const prefix = "ProseMirror-menubar"
|
||||
|
||||
function isIOS() {
|
||||
if (typeof navigator == "undefined") return false
|
||||
let agent = navigator.userAgent
|
||||
return !/Edge\/\d/.test(agent) && /AppleWebKit/.test(agent) && /Mobile\/\w+/.test(agent)
|
||||
}
|
||||
|
||||
// :: (Object) → Plugin
|
||||
// A plugin that will place a menu bar above the editor. Note that
|
||||
// this involves wrapping the editor in an additional `<div>`.
|
||||
//
|
||||
// options::-
|
||||
// Supports the following options:
|
||||
//
|
||||
// content:: [[MenuElement]]
|
||||
// Provides the content of the menu, as a nested array to be
|
||||
// passed to `renderGrouped`.
|
||||
//
|
||||
// floating:: ?bool
|
||||
// Determines whether the menu floats, i.e. whether it sticks to
|
||||
// the top of the viewport when the editor is partially scrolled
|
||||
// out of view.
|
||||
export function menuBar(options) {
|
||||
return new Plugin({
|
||||
view(editorView) { return new MenuBarView(editorView, options) }
|
||||
})
|
||||
}
|
||||
|
||||
class MenuBarView {
|
||||
constructor(editorView, options) {
|
||||
this.editorView = editorView
|
||||
this.options = options
|
||||
|
||||
this.wrapper = crel("div", {class: prefix + "-wrapper"})
|
||||
this.menu = this.wrapper.appendChild(crel("div", {class: prefix}))
|
||||
this.menu.className = prefix
|
||||
this.spacer = null
|
||||
|
||||
if (editorView.dom.parentNode)
|
||||
editorView.dom.parentNode.replaceChild(this.wrapper, editorView.dom)
|
||||
this.wrapper.appendChild(editorView.dom)
|
||||
|
||||
this.maxHeight = 0
|
||||
this.widthForMaxHeight = 0
|
||||
this.floating = false
|
||||
|
||||
let {dom, update} = renderGrouped(this.editorView, this.options.content)
|
||||
this.contentUpdate = update
|
||||
this.menu.appendChild(dom)
|
||||
this.update()
|
||||
|
||||
if (options.floating && !isIOS()) {
|
||||
this.updateFloat()
|
||||
let potentialScrollers = getAllWrapping(this.wrapper)
|
||||
this.scrollFunc = (e) => {
|
||||
let root = this.editorView.root
|
||||
if (!(root.body || root).contains(this.wrapper)) {
|
||||
potentialScrollers.forEach(el => el.removeEventListener("scroll", this.scrollFunc))
|
||||
} else {
|
||||
this.updateFloat(e.target.getBoundingClientRect && e.target)
|
||||
}
|
||||
}
|
||||
potentialScrollers.forEach(el => el.addEventListener('scroll', this.scrollFunc))
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.contentUpdate(this.editorView.state)
|
||||
|
||||
if (this.floating) {
|
||||
this.updateScrollCursor()
|
||||
} else {
|
||||
if (this.menu.offsetWidth != this.widthForMaxHeight) {
|
||||
this.widthForMaxHeight = this.menu.offsetWidth
|
||||
this.maxHeight = 0
|
||||
}
|
||||
if (this.menu.offsetHeight > this.maxHeight) {
|
||||
this.maxHeight = this.menu.offsetHeight
|
||||
this.menu.style.minHeight = this.maxHeight + "px"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateScrollCursor() {
|
||||
let selection = this.editorView.root.getSelection()
|
||||
if (!selection.focusNode) return
|
||||
let rects = selection.getRangeAt(0).getClientRects()
|
||||
let selRect = rects[selectionIsInverted(selection) ? 0 : rects.length - 1]
|
||||
if (!selRect) return
|
||||
let menuRect = this.menu.getBoundingClientRect()
|
||||
if (selRect.top < menuRect.bottom && selRect.bottom > menuRect.top) {
|
||||
let scrollable = findWrappingScrollable(this.wrapper)
|
||||
if (scrollable) scrollable.scrollTop -= (menuRect.bottom - selRect.top)
|
||||
}
|
||||
}
|
||||
|
||||
updateFloat(scrollAncestor) {
|
||||
let parent = this.wrapper, editorRect = parent.getBoundingClientRect(),
|
||||
top = scrollAncestor ? Math.max(0, scrollAncestor.getBoundingClientRect().top) : 0
|
||||
|
||||
if (this.floating) {
|
||||
if (editorRect.top >= top || editorRect.bottom < this.menu.offsetHeight + 10) {
|
||||
this.floating = false
|
||||
this.menu.style.position = this.menu.style.left = this.menu.style.top = this.menu.style.width = ""
|
||||
this.menu.style.display = ""
|
||||
this.spacer.parentNode.removeChild(this.spacer)
|
||||
this.spacer = null
|
||||
} else {
|
||||
let border = (parent.offsetWidth - parent.clientWidth) / 2
|
||||
this.menu.style.left = (editorRect.left + border) + "px"
|
||||
this.menu.style.display = (editorRect.top > window.innerHeight ? "none" : "")
|
||||
if (scrollAncestor) this.menu.style.top = top + "px"
|
||||
}
|
||||
} else {
|
||||
if (editorRect.top < top && editorRect.bottom >= this.menu.offsetHeight + 10) {
|
||||
this.floating = true
|
||||
let menuRect = this.menu.getBoundingClientRect()
|
||||
this.menu.style.left = menuRect.left + "px"
|
||||
this.menu.style.width = menuRect.width + "px"
|
||||
if (scrollAncestor) this.menu.style.top = top + "px"
|
||||
this.menu.style.position = "fixed"
|
||||
this.spacer = crel("div", {class: prefix + "-spacer", style: `height: ${menuRect.height}px`})
|
||||
parent.insertBefore(this.spacer, this.menu)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.wrapper.parentNode)
|
||||
this.wrapper.parentNode.replaceChild(this.editorView.dom, this.wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
// Not precise, but close enough
|
||||
function selectionIsInverted(selection) {
|
||||
if (selection.anchorNode == selection.focusNode) return selection.anchorOffset > selection.focusOffset
|
||||
return selection.anchorNode.compareDocumentPosition(selection.focusNode) == Node.DOCUMENT_POSITION_FOLLOWING
|
||||
}
|
||||
|
||||
function findWrappingScrollable(node) {
|
||||
for (let cur = node.parentNode; cur; cur = cur.parentNode)
|
||||
if (cur.scrollHeight > cur.clientHeight) return cur
|
||||
}
|
||||
|
||||
function getAllWrapping(node) {
|
||||
let res = [window]
|
||||
for (let cur = node.parentNode; cur; cur = cur.parentNode)
|
||||
res.push(cur)
|
||||
return res
|
||||
}
|
26
resources/js/editor/node-views/IframeView.js
Normal file
26
resources/js/editor/node-views/IframeView.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
class IframeView {
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {PmView} view
|
||||
* @param {(function(): number)} getPos
|
||||
*/
|
||||
constructor(node, view, getPos) {
|
||||
this.dom = document.createElement('div');
|
||||
this.dom.classList.add('ProseMirror-iframewrap');
|
||||
|
||||
this.iframe = document.createElement("iframe");
|
||||
for (const [key, value] of Object.entries(node.attrs)) {
|
||||
if (value) {
|
||||
this.iframe.setAttribute(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.dom.appendChild(this.iframe);
|
||||
}
|
||||
|
||||
stopEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default IframeView;
|
197
resources/js/editor/node-views/ImageView.js
Normal file
197
resources/js/editor/node-views/ImageView.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
import {positionHandlesAtCorners, removeHandles, renderHandlesAtCorners} from "./node-view-utils";
|
||||
import {NodeSelection} from "prosemirror-state";
|
||||
|
||||
class ImageView {
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {PmView} view
|
||||
* @param {(function(): number)} getPos
|
||||
*/
|
||||
constructor(node, view, getPos) {
|
||||
this.dom = document.createElement('div');
|
||||
this.dom.classList.add('ProseMirror-imagewrap');
|
||||
|
||||
this.image = document.createElement("img");
|
||||
this.image.src = node.attrs.src;
|
||||
this.image.alt = node.attrs.alt;
|
||||
if (node.attrs.width) {
|
||||
this.image.width = node.attrs.width;
|
||||
}
|
||||
if (node.attrs.height) {
|
||||
this.image.height = node.attrs.height;
|
||||
}
|
||||
|
||||
this.dom.appendChild(this.image);
|
||||
|
||||
this.handles = [];
|
||||
this.handleDragStartInfo = null;
|
||||
this.handleDragMoveDimensions = null;
|
||||
this.removeHandlesListener = this.removeHandlesListener.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleMouseUp = this.handleMouseUp.bind(this);
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
|
||||
this.dom.addEventListener("click", event => {
|
||||
this.showHandles();
|
||||
});
|
||||
|
||||
// Show handles if selected
|
||||
if (view.state.selection.node === node) {
|
||||
window.setTimeout(() => {
|
||||
this.showHandles();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.updateImageDimensions = function (width, height) {
|
||||
const attrs = Object.assign({}, node.attrs, {width, height});
|
||||
let tr = view.state.tr;
|
||||
const position = getPos();
|
||||
tr = tr.setNodeMarkup(position, null, attrs)
|
||||
tr = tr.setSelection(NodeSelection.create(tr.doc, position));
|
||||
view.dispatch(tr);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
showHandles() {
|
||||
if (this.handles.length === 0) {
|
||||
this.image.dataset.showHandles = 'true';
|
||||
window.addEventListener('click', this.removeHandlesListener);
|
||||
this.handles = renderHandlesAtCorners(this.image);
|
||||
for (const handle of this.handles) {
|
||||
handle.addEventListener('mousedown', this.handleMouseDown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeHandlesListener(event) {
|
||||
if (!this.dom.contains(event.target)) {
|
||||
this.removeHandles();
|
||||
this.handles = [];
|
||||
}
|
||||
}
|
||||
|
||||
removeHandles() {
|
||||
removeHandles(this.handles);
|
||||
window.removeEventListener('click', this.removeHandlesListener);
|
||||
delete this.image.dataset.showHandles;
|
||||
}
|
||||
|
||||
stopEvent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMouseDown(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const imageBounds = this.image.getBoundingClientRect();
|
||||
const handle = event.target;
|
||||
this.handleDragStartInfo = {
|
||||
x: event.screenX,
|
||||
y: event.screenY,
|
||||
ratio: imageBounds.width / imageBounds.height,
|
||||
bounds: imageBounds,
|
||||
handleX: handle.dataset.x,
|
||||
handleY: handle.dataset.y,
|
||||
};
|
||||
|
||||
this.createDragDummy(imageBounds);
|
||||
this.dom.appendChild(this.dragDummy);
|
||||
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DOMRect} bounds
|
||||
*/
|
||||
createDragDummy(bounds) {
|
||||
this.dragDummy = this.image.cloneNode();
|
||||
this.dragDummy.style.opacity = '0.5';
|
||||
this.dragDummy.classList.add('ProseMirror-dragdummy');
|
||||
this.dragDummy.style.width = bounds.width + 'px';
|
||||
this.dragDummy.style.height = bounds.height + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMouseUp(event) {
|
||||
if (this.handleDragMoveDimensions) {
|
||||
const {width, height} = this.handleDragMoveDimensions;
|
||||
this.updateImageDimensions(String(width), String(height));
|
||||
}
|
||||
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
this.handleDragStartInfo = null;
|
||||
this.handleDragMoveDimensions = null;
|
||||
this.dragDummy.remove();
|
||||
positionHandlesAtCorners(this.image, this.handles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
handleMouseMove(event) {
|
||||
const originalBounds = this.handleDragStartInfo.bounds;
|
||||
|
||||
// Calculate change in x & y, flip amounts depending on handle
|
||||
let xChange = event.screenX - this.handleDragStartInfo.x;
|
||||
if (this.handleDragStartInfo.handleX === 'left') {
|
||||
xChange = -xChange;
|
||||
}
|
||||
let yChange = event.screenY - this.handleDragStartInfo.y;
|
||||
if (this.handleDragStartInfo.handleY === 'top') {
|
||||
yChange = -yChange;
|
||||
}
|
||||
|
||||
// Prevent images going too small or into negative bounds
|
||||
if (originalBounds.width + xChange < 10) {
|
||||
xChange = -originalBounds.width + 10;
|
||||
}
|
||||
if (originalBounds.height + yChange < 10) {
|
||||
yChange = -originalBounds.height + 10;
|
||||
}
|
||||
|
||||
// Choose the larger dimension change and align the other to keep
|
||||
// image aspect ratio, aligning growth/reduction direction
|
||||
if (Math.abs(xChange) > Math.abs(yChange)) {
|
||||
yChange = Math.floor(xChange * this.handleDragStartInfo.ratio);
|
||||
if (yChange * xChange < 0) {
|
||||
yChange = -yChange;
|
||||
}
|
||||
} else {
|
||||
xChange = Math.floor(yChange / this.handleDragStartInfo.ratio);
|
||||
if (xChange * yChange < 0) {
|
||||
xChange = -xChange;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate our new sizes
|
||||
const newWidth = originalBounds.width + xChange;
|
||||
const newHeight = originalBounds.height + yChange;
|
||||
|
||||
// Apply the sizes and positioning to our ghost dummy
|
||||
this.dragDummy.style.width = `${newWidth}px`;
|
||||
if (this.handleDragStartInfo.handleX === 'left') {
|
||||
this.dragDummy.style.left = `${-xChange}px`;
|
||||
}
|
||||
this.dragDummy.style.height = `${newHeight}px`;
|
||||
if (this.handleDragStartInfo.handleY === 'top') {
|
||||
this.dragDummy.style.top = `${-yChange}px`;
|
||||
}
|
||||
|
||||
// Update corners and track dimension changes for later application
|
||||
positionHandlesAtCorners(this.dragDummy, this.handles);
|
||||
this.handleDragMoveDimensions = {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageView;
|
21
resources/js/editor/node-views/TableView.js
Normal file
21
resources/js/editor/node-views/TableView.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
class TableView {
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {PmView} view
|
||||
* @param {(function(): number)} getPos
|
||||
*/
|
||||
constructor(node, view, getPos) {
|
||||
this.dom = document.createElement("div")
|
||||
this.dom.className = "ProseMirror-tableWrapper"
|
||||
this.table = this.dom.appendChild(document.createElement("table"));
|
||||
this.table.setAttribute('style', node.attrs.style);
|
||||
this.colgroup = this.table.appendChild(document.createElement("colgroup"));
|
||||
this.contentDOM = this.table.appendChild(document.createElement("tbody"));
|
||||
}
|
||||
|
||||
ignoreMutation(record) {
|
||||
return record.type == "attributes" && (record.target == this.table || this.colgroup.contains(record.target))
|
||||
}
|
||||
}
|
||||
|
||||
export default TableView;
|
11
resources/js/editor/node-views/index.js
Normal file
11
resources/js/editor/node-views/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import ImageView from "./ImageView";
|
||||
import IframeView from "./IframeView";
|
||||
import TableView from "./TableView";
|
||||
|
||||
const views = {
|
||||
image: (node, view, getPos) => new ImageView(node, view, getPos),
|
||||
iframe: (node, view, getPos) => new IframeView(node, view, getPos),
|
||||
table: (node, view, getPos) => new TableView(node, view, getPos),
|
||||
};
|
||||
|
||||
export default views;
|
58
resources/js/editor/node-views/node-view-utils.js
Normal file
58
resources/js/editor/node-views/node-view-utils.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import crel from "crelt";
|
||||
|
||||
/**
|
||||
* Render grab handles at the corners of the given element.
|
||||
* @param {Element} elem
|
||||
* @return {Element[]}
|
||||
*/
|
||||
export function renderHandlesAtCorners(elem) {
|
||||
const handles = [];
|
||||
const baseClass = 'ProseMirror-grabhandle';
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const y = (i < 2) ? 'top' : 'bottom';
|
||||
const x = (i === 0 || i === 3) ? 'left' : 'right';
|
||||
const handle = crel('div', {
|
||||
class: `${baseClass} ${baseClass}-${x}-${y}`,
|
||||
});
|
||||
handle.dataset.y = y;
|
||||
handle.dataset.x = x;
|
||||
handles.push(handle);
|
||||
elem.parentNode.appendChild(handle);
|
||||
}
|
||||
|
||||
positionHandlesAtCorners(elem, handles);
|
||||
return handles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element[]} handles
|
||||
*/
|
||||
export function removeHandles(handles) {
|
||||
for (const handle of handles) {
|
||||
handle.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Element} element
|
||||
* @param {[Element, Element, Element, Element]}handles
|
||||
*/
|
||||
export function positionHandlesAtCorners(element, handles) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
const parentBounds = element.parentElement.getBoundingClientRect();
|
||||
const positions = [
|
||||
{x: bounds.left - parentBounds.left, y: bounds.top - parentBounds.top},
|
||||
{x: bounds.right - parentBounds.left, y: bounds.top - parentBounds.top},
|
||||
{x: bounds.right - parentBounds.left, y: bounds.bottom - parentBounds.top},
|
||||
{x: bounds.left - parentBounds.left, y: bounds.bottom - parentBounds.top},
|
||||
];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const {x, y} = positions[i];
|
||||
const handle = handles[i];
|
||||
handle.style.left = (x - 6) + 'px';
|
||||
handle.style.top = (y - 6) + 'px';
|
||||
}
|
||||
}
|
288
resources/js/editor/plugins/table-resizing.js
Normal file
288
resources/js/editor/plugins/table-resizing.js
Normal file
|
@ -0,0 +1,288 @@
|
|||
/**
|
||||
* This file originates from https://github.com/ProseMirror/prosemirror-tables
|
||||
* and is hence subject to the MIT license found here:
|
||||
* https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
* @copyright Marijn Haverbeke and others
|
||||
*/
|
||||
|
||||
import {Plugin, PluginKey} from "prosemirror-state"
|
||||
import {Decoration, DecorationSet} from "prosemirror-view"
|
||||
import {
|
||||
cellAround,
|
||||
pointsAtCell,
|
||||
setAttr,
|
||||
TableMap,
|
||||
} from "prosemirror-tables";
|
||||
|
||||
export const key = new PluginKey("tableColumnResizing")
|
||||
|
||||
export function columnResizing(options = {}) {
|
||||
const {
|
||||
handleWidth, cellMinWidth, lastColumnResizable
|
||||
} = Object.assign({
|
||||
handleWidth: 5,
|
||||
cellMinWidth: 25,
|
||||
lastColumnResizable: true
|
||||
}, options);
|
||||
|
||||
let plugin = new Plugin({
|
||||
key,
|
||||
state: {
|
||||
init(_, state) {
|
||||
return new ResizeState(-1, false)
|
||||
},
|
||||
apply(tr, prev) {
|
||||
return prev.apply(tr)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
attributes(state) {
|
||||
let pluginState = key.getState(state)
|
||||
return pluginState.activeHandle > -1 ? {class: "resize-cursor"} : null
|
||||
},
|
||||
|
||||
handleDOMEvents: {
|
||||
mousemove(view, event) {
|
||||
handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable)
|
||||
},
|
||||
mouseleave(view) {
|
||||
handleMouseLeave(view)
|
||||
},
|
||||
mousedown(view, event) {
|
||||
handleMouseDown(view, event, cellMinWidth)
|
||||
}
|
||||
},
|
||||
|
||||
decorations(state) {
|
||||
let pluginState = key.getState(state)
|
||||
if (pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle)
|
||||
},
|
||||
|
||||
nodeViews: {}
|
||||
}
|
||||
})
|
||||
return plugin
|
||||
}
|
||||
|
||||
class ResizeState {
|
||||
constructor(activeHandle, dragging) {
|
||||
this.activeHandle = activeHandle
|
||||
this.dragging = dragging
|
||||
}
|
||||
|
||||
apply(tr) {
|
||||
let state = this, action = tr.getMeta(key)
|
||||
if (action && action.setHandle != null)
|
||||
return new ResizeState(action.setHandle, null)
|
||||
if (action && action.setDragging !== undefined)
|
||||
return new ResizeState(state.activeHandle, action.setDragging)
|
||||
if (state.activeHandle > -1 && tr.docChanged) {
|
||||
let handle = tr.mapping.map(state.activeHandle, -1)
|
||||
if (!pointsAtCell(tr.doc.resolve(handle))) handle = null
|
||||
state = new ResizeState(handle, state.dragging)
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(view, event, handleWidth, cellMinWidth, lastColumnResizable) {
|
||||
let pluginState = key.getState(view.state)
|
||||
|
||||
if (!pluginState.dragging) {
|
||||
let target = domCellAround(event.target), cell = -1
|
||||
if (target) {
|
||||
let {left, right} = target.getBoundingClientRect()
|
||||
if (event.clientX - left <= handleWidth)
|
||||
cell = edgeCell(view, event, "left")
|
||||
else if (right - event.clientX <= handleWidth)
|
||||
cell = edgeCell(view, event, "right")
|
||||
}
|
||||
|
||||
if (cell != pluginState.activeHandle) {
|
||||
if (!lastColumnResizable && cell !== -1) {
|
||||
let $cell = view.state.doc.resolve(cell)
|
||||
let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
|
||||
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
|
||||
|
||||
if (col == map.width - 1) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updateHandle(view, cell)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseLeave(view) {
|
||||
let pluginState = key.getState(view.state)
|
||||
if (pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1)
|
||||
}
|
||||
|
||||
function handleMouseDown(view, event, cellMinWidth) {
|
||||
let pluginState = key.getState(view.state)
|
||||
if (pluginState.activeHandle == -1 || pluginState.dragging) return false
|
||||
|
||||
let cell = view.state.doc.nodeAt(pluginState.activeHandle)
|
||||
let width = currentColWidth(view, pluginState.activeHandle, cell.attrs)
|
||||
view.dispatch(view.state.tr.setMeta(key, {setDragging: {startX: event.clientX, startWidth: width}}))
|
||||
|
||||
function finish(event) {
|
||||
window.removeEventListener("mouseup", finish)
|
||||
window.removeEventListener("mousemove", move)
|
||||
let pluginState = key.getState(view.state)
|
||||
if (pluginState.dragging) {
|
||||
updateColumnWidth(view, pluginState.activeHandle, draggedWidth(pluginState.dragging, event, cellMinWidth))
|
||||
view.dispatch(view.state.tr.setMeta(key, {setDragging: null}))
|
||||
}
|
||||
}
|
||||
|
||||
function move(event) {
|
||||
if (!event.which) return finish(event)
|
||||
let pluginState = key.getState(view.state)
|
||||
let dragged = draggedWidth(pluginState.dragging, event, cellMinWidth)
|
||||
displayColumnWidth(view, pluginState.activeHandle, dragged, cellMinWidth)
|
||||
}
|
||||
|
||||
window.addEventListener("mouseup", finish)
|
||||
window.addEventListener("mousemove", move)
|
||||
event.preventDefault()
|
||||
return true
|
||||
}
|
||||
|
||||
function currentColWidth(view, cellPos, {colspan, colwidth}) {
|
||||
let width = colwidth && colwidth[colwidth.length - 1]
|
||||
if (width) return width
|
||||
let dom = view.domAtPos(cellPos)
|
||||
let node = dom.node.childNodes[dom.offset]
|
||||
let domWidth = node.offsetWidth, parts = colspan
|
||||
if (colwidth) for (let i = 0; i < colspan; i++) if (colwidth[i]) {
|
||||
domWidth -= colwidth[i]
|
||||
parts--
|
||||
}
|
||||
return domWidth / parts
|
||||
}
|
||||
|
||||
function domCellAround(target) {
|
||||
while (target && target.nodeName != "TD" && target.nodeName != "TH")
|
||||
target = target.classList.contains("ProseMirror") ? null : target.parentNode
|
||||
return target
|
||||
}
|
||||
|
||||
function edgeCell(view, event, side) {
|
||||
let found = view.posAtCoords({left: event.clientX, top: event.clientY})
|
||||
if (!found) return -1
|
||||
let {pos} = found
|
||||
let $cell = cellAround(view.state.doc.resolve(pos))
|
||||
if (!$cell) return -1
|
||||
if (side == "right") return $cell.pos
|
||||
let map = TableMap.get($cell.node(-1)), start = $cell.start(-1)
|
||||
let index = map.map.indexOf($cell.pos - start)
|
||||
return index % map.width == 0 ? -1 : start + map.map[index - 1]
|
||||
}
|
||||
|
||||
function draggedWidth(dragging, event, cellMinWidth) {
|
||||
let offset = event.clientX - dragging.startX
|
||||
return Math.max(cellMinWidth, dragging.startWidth + offset)
|
||||
}
|
||||
|
||||
function updateHandle(view, value) {
|
||||
view.dispatch(view.state.tr.setMeta(key, {setHandle: value}))
|
||||
}
|
||||
|
||||
function updateColumnWidth(view, cell, width) {
|
||||
let $cell = view.state.doc.resolve(cell);
|
||||
let table = $cell.node(-1);
|
||||
let map = TableMap.get(table);
|
||||
let start = $cell.start(-1);
|
||||
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1;
|
||||
let tr = view.state.tr;
|
||||
|
||||
for (let row = 0; row < map.height; row++) {
|
||||
let mapIndex = row * map.width + col;
|
||||
// Rowspanning cell that has already been handled
|
||||
if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue
|
||||
let pos = map.map[mapIndex]
|
||||
let {attrs} = table.nodeAt(pos);
|
||||
const newWidth = (attrs.colspan * width) + 'px';
|
||||
|
||||
tr.setNodeMarkup(start + pos, null, setAttr(attrs, "width", newWidth));
|
||||
}
|
||||
|
||||
if (tr.docChanged) view.dispatch(tr)
|
||||
}
|
||||
|
||||
function displayColumnWidth(view, cell, width, cellMinWidth) {
|
||||
const $cell = view.state.doc.resolve(cell)
|
||||
const table = $cell.node(-1);
|
||||
const start = $cell.start(-1);
|
||||
const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1
|
||||
let dom = view.domAtPos($cell.start(-1)).node
|
||||
while (dom.nodeName !== "TABLE") {
|
||||
dom = dom.parentNode
|
||||
}
|
||||
updateColumnsOnResize(view, table, dom, cellMinWidth, col, width)
|
||||
}
|
||||
|
||||
|
||||
function updateColumnsOnResize(view, tableNode, tableDom, cellMinWidth, overrideCol, overrideValue) {
|
||||
console.log({tableNode, tableDom, cellMinWidth, overrideCol, overrideValue});
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
const rows = tableDom.querySelectorAll('tr');
|
||||
|
||||
for (let y = 0; y < rows.length; y++) {
|
||||
const row = rows[y];
|
||||
const cell = row.children[overrideCol];
|
||||
cell.style.width = `${overrideValue}px`;
|
||||
if (y === 0) {
|
||||
for (let x = 0; x < row.children.length; x++) {
|
||||
const cell = row.children[x];
|
||||
if (cell.style.width) {
|
||||
const width = Number(cell.style.width.replace('px', ''));
|
||||
totalWidth += width || cellMinWidth;
|
||||
} else {
|
||||
fixedWidth = false;
|
||||
totalWidth += cellMinWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(totalWidth);
|
||||
if (fixedWidth) {
|
||||
tableDom.style.width = totalWidth + "px"
|
||||
tableDom.style.minWidth = ""
|
||||
} else {
|
||||
tableDom.style.width = ""
|
||||
tableDom.style.minWidth = totalWidth + "px"
|
||||
}
|
||||
}
|
||||
|
||||
function zeroes(n) {
|
||||
let result = []
|
||||
for (let i = 0; i < n; i++) result.push(0)
|
||||
return result
|
||||
}
|
||||
|
||||
function handleDecorations(state, cell) {
|
||||
let decorations = []
|
||||
let $cell = state.doc.resolve(cell)
|
||||
let table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1)
|
||||
let col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan
|
||||
for (let row = 0; row < map.height; row++) {
|
||||
let index = col + row * map.width - 1
|
||||
// For positions that are have either a different cell or the end
|
||||
// of the table to their right, and either the top of the table or
|
||||
// a different cell above them, add a decoration
|
||||
if ((col == map.width || map.map[index] != map.map[index + 1]) &&
|
||||
(row == 0 || map.map[index - 1] != map.map[index - 1 - map.width])) {
|
||||
let cellPos = map.map[index]
|
||||
let pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1
|
||||
let dom = document.createElement("div")
|
||||
dom.className = "column-resize-handle"
|
||||
decorations.push(Decoration.widget(pos, dom))
|
||||
}
|
||||
}
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
}
|
131
resources/js/editor/schema-marks.js
Normal file
131
resources/js/editor/schema-marks.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
const link = {
|
||||
attrs: {
|
||||
href: {},
|
||||
title: {default: null},
|
||||
target: {default: null}
|
||||
},
|
||||
inclusive: false,
|
||||
parseDOM: [{
|
||||
tag: "a[href]", getAttrs: function getAttrs(dom) {
|
||||
return {
|
||||
href: dom.getAttribute("href"),
|
||||
title: dom.getAttribute("title"),
|
||||
target: dom.getAttribute("target"),
|
||||
}
|
||||
}
|
||||
}],
|
||||
toDOM: function toDOM(node) {
|
||||
const ref = node.attrs;
|
||||
const href = ref.href;
|
||||
const title = ref.title;
|
||||
const target = ref.target;
|
||||
return ["a", {href, title, target}, 0]
|
||||
}
|
||||
};
|
||||
|
||||
const em = {
|
||||
parseDOM: [{tag: "i"}, {tag: "em"}, {style: "font-style=italic"}],
|
||||
toDOM() {
|
||||
return ["em", 0]
|
||||
}
|
||||
};
|
||||
|
||||
const strong = {
|
||||
parseDOM: [{tag: "strong"},
|
||||
// This works around a Google Docs misbehavior where
|
||||
// pasted content will be inexplicably wrapped in `<b>`
|
||||
// tags with a font-weight normal.
|
||||
{
|
||||
tag: "b", getAttrs: function (node) {
|
||||
return node.style.fontWeight != "normal" && null;
|
||||
}
|
||||
},
|
||||
{
|
||||
style: "font-weight", getAttrs: function (value) {
|
||||
return /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null;
|
||||
}
|
||||
}],
|
||||
toDOM() {
|
||||
return ["strong", 0]
|
||||
}
|
||||
};
|
||||
|
||||
const code = {
|
||||
parseDOM: [{tag: "code"}],
|
||||
toDOM() {
|
||||
return ["code", 0]
|
||||
}
|
||||
};
|
||||
|
||||
const underline = {
|
||||
parseDOM: [{tag: "u"}, {style: "text-decoration=underline"}],
|
||||
toDOM() {
|
||||
return ["span", {style: "text-decoration: underline;"}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const strike = {
|
||||
parseDOM: [{tag: "s"}, {tag: "strike"}, {style: "text-decoration=line-through"}],
|
||||
toDOM() {
|
||||
return ["span", {style: "text-decoration: line-through;"}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const superscript = {
|
||||
parseDOM: [{tag: "sup"}],
|
||||
toDOM() {
|
||||
return ["sup", 0];
|
||||
}
|
||||
};
|
||||
|
||||
const subscript = {
|
||||
parseDOM: [{tag: "sub"}],
|
||||
toDOM() {
|
||||
return ["sub", 0];
|
||||
}
|
||||
};
|
||||
|
||||
const text_color = {
|
||||
attrs: {
|
||||
color: {},
|
||||
},
|
||||
parseDOM: [{
|
||||
style: 'color',
|
||||
getAttrs(color) {
|
||||
return {color}
|
||||
}
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ['span', {style: `color: ${node.attrs.color};`}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const background_color = {
|
||||
attrs: {
|
||||
color: {},
|
||||
},
|
||||
parseDOM: [{
|
||||
style: 'background-color',
|
||||
getAttrs(color) {
|
||||
return {color}
|
||||
}
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ['span', {style: `background-color: ${node.attrs.color};`}, 0];
|
||||
}
|
||||
};
|
||||
|
||||
const marks = {
|
||||
link,
|
||||
em,
|
||||
strong,
|
||||
code,
|
||||
underline,
|
||||
strike,
|
||||
superscript,
|
||||
subscript,
|
||||
text_color,
|
||||
background_color,
|
||||
};
|
||||
|
||||
export default marks;
|
383
resources/js/editor/schema-nodes.js
Normal file
383
resources/js/editor/schema-nodes.js
Normal file
|
@ -0,0 +1,383 @@
|
|||
import {orderedList, bulletList, listItem} from "prosemirror-schema-list";
|
||||
import {Fragment} from "prosemirror-model";
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} node
|
||||
* @return {string|null}
|
||||
*/
|
||||
function getAlignAttrFromDomNode(node) {
|
||||
const classList = node.classList;
|
||||
const styles = node.style || {};
|
||||
const alignments = ['right', 'left', 'center', 'justify'];
|
||||
for (const alignment of alignments) {
|
||||
if (classList.contains('align-' + alignment) || styles.textAlign === alignment) {
|
||||
return alignment;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param node
|
||||
* @param {Object} attrs
|
||||
* @return {Object}
|
||||
*/
|
||||
function addAlignmentAttr(node, attrs) {
|
||||
const positions = ['right', 'left', 'center', 'justify'];
|
||||
for (const position of positions) {
|
||||
if (node.attrs.align === position) {
|
||||
return addClassToAttrs('align-' + position, attrs);
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function getAttrsParserForAlignment(node) {
|
||||
return {
|
||||
align: getAlignAttrFromDomNode(node),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} className
|
||||
* @param {Object} attrs
|
||||
* @return {Object}
|
||||
*/
|
||||
function addClassToAttrs(className, attrs) {
|
||||
return Object.assign({}, attrs, {
|
||||
class: attrs.class ? attrs.class + ' ' + className : className,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String[]} attrNames
|
||||
* @return {function(Element): {}}
|
||||
*/
|
||||
function domAttrsToAttrsParser(attrNames) {
|
||||
return function (node) {
|
||||
const attrs = {};
|
||||
for (const attr of attrNames) {
|
||||
attrs[attr] = node.hasAttribute(attr) ? node.getAttribute(attr) : null;
|
||||
}
|
||||
return attrs;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmNode} node
|
||||
* @param {String[]} attrNames
|
||||
*/
|
||||
function extractAttrsForDom(node, attrNames) {
|
||||
const domAttrs = {};
|
||||
for (const attr of attrNames) {
|
||||
if (node.attrs[attr]) {
|
||||
domAttrs[attr] = node.attrs[attr];
|
||||
}
|
||||
}
|
||||
return domAttrs;
|
||||
}
|
||||
|
||||
const doc = {
|
||||
content: "block+",
|
||||
};
|
||||
|
||||
const paragraph = {
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
parseDOM: [
|
||||
{
|
||||
tag: "p",
|
||||
getAttrs: getAttrsParserForAlignment,
|
||||
}
|
||||
],
|
||||
attrs: {
|
||||
align: {
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
toDOM(node) {
|
||||
return ["p", addAlignmentAttr(node, {}), 0];
|
||||
}
|
||||
};
|
||||
|
||||
const blockquote = {
|
||||
content: "block+",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [{tag: "blockquote", getAttrs: getAttrsParserForAlignment}],
|
||||
attrs: {
|
||||
align: {
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
toDOM(node) {
|
||||
return ["blockquote", addAlignmentAttr(node, {}), 0];
|
||||
}
|
||||
};
|
||||
|
||||
const horizontal_rule = {
|
||||
group: "block",
|
||||
parseDOM: [{tag: "hr"}],
|
||||
toDOM() {
|
||||
return ["hr"];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const headingParseGetAttrs = (level) => {
|
||||
return function (node) {
|
||||
return {level, align: getAlignAttrFromDomNode(node)};
|
||||
};
|
||||
};
|
||||
const heading = {
|
||||
attrs: {level: {default: 1}, align: {default: null}},
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [
|
||||
{tag: "h1", getAttrs: headingParseGetAttrs(1)},
|
||||
{tag: "h2", getAttrs: headingParseGetAttrs(2)},
|
||||
{tag: "h3", getAttrs: headingParseGetAttrs(3)},
|
||||
{tag: "h4", getAttrs: headingParseGetAttrs(4)},
|
||||
{tag: "h5", getAttrs: headingParseGetAttrs(5)},
|
||||
{tag: "h6", getAttrs: headingParseGetAttrs(6)},
|
||||
],
|
||||
toDOM(node) {
|
||||
return ["h" + node.attrs.level, addAlignmentAttr(node, {}), 0]
|
||||
}
|
||||
};
|
||||
|
||||
const code_block = {
|
||||
content: "text*",
|
||||
marks: "",
|
||||
group: "block",
|
||||
code: true,
|
||||
defining: true,
|
||||
parseDOM: [{tag: "pre", preserveWhitespace: "full"}],
|
||||
toDOM() {
|
||||
return ["pre", ["code", 0]];
|
||||
}
|
||||
};
|
||||
|
||||
const text = {
|
||||
group: "inline"
|
||||
};
|
||||
|
||||
const image = {
|
||||
inline: true,
|
||||
attrs: {
|
||||
src: {},
|
||||
alt: {default: null},
|
||||
title: {default: null},
|
||||
height: {default: null},
|
||||
width: {default: null},
|
||||
},
|
||||
group: "inline",
|
||||
draggable: true,
|
||||
parseDOM: [{
|
||||
tag: "img[src]", getAttrs: function getAttrs(dom) {
|
||||
return {
|
||||
src: dom.getAttribute("src"),
|
||||
title: dom.getAttribute("title"),
|
||||
alt: dom.getAttribute("alt"),
|
||||
height: dom.getAttribute("height"),
|
||||
width: dom.getAttribute("width"),
|
||||
}
|
||||
}
|
||||
}],
|
||||
toDOM: function toDOM(node) {
|
||||
const ref = node.attrs;
|
||||
const src = ref.src;
|
||||
const alt = ref.alt;
|
||||
const title = ref.title;
|
||||
const width = ref.width;
|
||||
const height = ref.height;
|
||||
return ["img", {src, alt, title, width, height}]
|
||||
}
|
||||
};
|
||||
|
||||
const iframe = {
|
||||
attrs: {
|
||||
src: {},
|
||||
height: {default: null},
|
||||
width: {default: null},
|
||||
title: {default: null},
|
||||
allow: {default: null},
|
||||
sandbox: {default: null},
|
||||
},
|
||||
group: "block",
|
||||
draggable: true,
|
||||
parseDOM: [{
|
||||
tag: "iframe",
|
||||
getAttrs: domAttrsToAttrsParser(["src", "width", "height", "title", "allow", "sandbox"]),
|
||||
}],
|
||||
toDOM(node) {
|
||||
const attrs = extractAttrsForDom(node, ["src", "width", "height", "title", "allow", "sandbox"])
|
||||
return ["iframe", attrs];
|
||||
}
|
||||
};
|
||||
|
||||
const hard_break = {
|
||||
inline: true,
|
||||
group: "inline",
|
||||
selectable: false,
|
||||
parseDOM: [{tag: "br"}],
|
||||
toDOM() {
|
||||
return ["br"];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const calloutParseGetAttrs = (type) => {
|
||||
return function (node) {
|
||||
return {type, align: getAlignAttrFromDomNode(node)};
|
||||
};
|
||||
};
|
||||
const callout = {
|
||||
attrs: {
|
||||
type: {default: 'info'},
|
||||
align: {default: null},
|
||||
},
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
defining: true,
|
||||
parseDOM: [
|
||||
{tag: 'p.callout.info', getAttrs: calloutParseGetAttrs('info'), priority: 75},
|
||||
{tag: 'p.callout.success', getAttrs: calloutParseGetAttrs('success'), priority: 75},
|
||||
{tag: 'p.callout.danger', getAttrs: calloutParseGetAttrs('danger'), priority: 75},
|
||||
{tag: 'p.callout.warning', getAttrs: calloutParseGetAttrs('warning'), priority: 75},
|
||||
{tag: 'p.callout', getAttrs: calloutParseGetAttrs('info'), priority: 75},
|
||||
],
|
||||
toDOM(node) {
|
||||
const type = node.attrs.type || 'info';
|
||||
return ['p', addAlignmentAttr(node, {class: 'callout ' + type}), 0];
|
||||
}
|
||||
};
|
||||
|
||||
const ordered_list = Object.assign({}, orderedList, {content: "list_item+", group: "block"});
|
||||
const bullet_list = Object.assign({}, bulletList, {content: "list_item+", group: "block"});
|
||||
const list_item = Object.assign({}, listItem, {content: 'paragraph block*'});
|
||||
|
||||
const table = {
|
||||
content: "table_row+",
|
||||
attrs: {
|
||||
style: {default: null},
|
||||
},
|
||||
tableRole: "table",
|
||||
isolating: true,
|
||||
group: "block",
|
||||
parseDOM: [{tag: "table", getAttrs: domAttrsToAttrsParser(['style'])}],
|
||||
toDOM(node) {
|
||||
return ["table", extractAttrsForDom(node, ['style']), ["tbody", 0]]
|
||||
}
|
||||
};
|
||||
|
||||
const table_row = {
|
||||
content: "(table_cell | table_header)*",
|
||||
tableRole: "row",
|
||||
parseDOM: [{tag: "tr"}],
|
||||
toDOM() { return ["tr", 0] }
|
||||
};
|
||||
|
||||
let cellAttrs = {
|
||||
colspan: {default: 1},
|
||||
rowspan: {default: 1},
|
||||
width: {default: null},
|
||||
height: {default: null},
|
||||
};
|
||||
|
||||
function getCellAttrs(dom) {
|
||||
return {
|
||||
colspan: Number(dom.getAttribute("colspan") || 1),
|
||||
rowspan: Number(dom.getAttribute("rowspan") || 1),
|
||||
width: dom.style.width || null,
|
||||
height: dom.style.height || null,
|
||||
};
|
||||
}
|
||||
|
||||
function setCellAttrs(node) {
|
||||
let attrs = {};
|
||||
|
||||
const styles = [];
|
||||
if (node.attrs.colspan != 1) attrs.colspan = node.attrs.colspan;
|
||||
if (node.attrs.rowspan != 1) attrs.rowspan = node.attrs.rowspan;
|
||||
if (node.attrs.width) styles.push(`width: ${node.attrs.width}`);
|
||||
if (node.attrs.height) styles.push(`height: ${node.attrs.height}`);
|
||||
if (styles) {
|
||||
attrs.style = styles.join(';');
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
const table_cell = {
|
||||
content: "block+",
|
||||
attrs: cellAttrs,
|
||||
tableRole: "cell",
|
||||
isolating: true,
|
||||
parseDOM: [{tag: "td", getAttrs: dom => getCellAttrs(dom)}],
|
||||
toDOM(node) { return ["td", setCellAttrs(node), 0] }
|
||||
};
|
||||
|
||||
const table_header = {
|
||||
content: "block+",
|
||||
attrs: cellAttrs,
|
||||
tableRole: "header_cell",
|
||||
isolating: true,
|
||||
parseDOM: [{tag: "th", getAttrs: dom => getCellAttrs(dom)}],
|
||||
toDOM(node) { return ["th", setCellAttrs(node), 0] }
|
||||
};
|
||||
|
||||
|
||||
const details = {
|
||||
content: "details_summary block*",
|
||||
isolating: true,
|
||||
group: "block",
|
||||
parseDOM: [{
|
||||
tag: "details",
|
||||
getAttrs(domNode) {
|
||||
return {}
|
||||
},
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ["details", 0];
|
||||
}
|
||||
};
|
||||
|
||||
const details_summary = {
|
||||
content: "inline*",
|
||||
group: "block",
|
||||
parseDOM: [{
|
||||
tag: "details summary",
|
||||
}],
|
||||
toDOM(node) {
|
||||
return ["summary", 0];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const nodes = {
|
||||
doc,
|
||||
paragraph,
|
||||
blockquote,
|
||||
horizontal_rule,
|
||||
heading,
|
||||
code_block,
|
||||
text,
|
||||
image,
|
||||
iframe,
|
||||
hard_break,
|
||||
callout,
|
||||
ordered_list,
|
||||
bullet_list,
|
||||
list_item,
|
||||
table,
|
||||
table_row,
|
||||
table_cell,
|
||||
table_header,
|
||||
details,
|
||||
details_summary,
|
||||
};
|
||||
|
||||
export default nodes;
|
12
resources/js/editor/schema.js
Normal file
12
resources/js/editor/schema.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import {Schema} from "prosemirror-model";
|
||||
|
||||
import nodes from "./schema-nodes";
|
||||
import marks from "./schema-marks";
|
||||
|
||||
/** @var {PmSchema} schema */
|
||||
const schema = new Schema({
|
||||
nodes,
|
||||
marks,
|
||||
});
|
||||
|
||||
export default schema;
|
106
resources/js/editor/types.js
Normal file
106
resources/js/editor/types.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* @typedef {Object} PmEditorState
|
||||
* @property {PmNode} doc
|
||||
* @property {PmSelection} selection
|
||||
* @property {PmMark[]|null} storedMarks
|
||||
* @property {PmSchema} schema
|
||||
* @property {PmTransaction} tr
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmNode
|
||||
* @property {PmNodeType} type
|
||||
* @property {Object} attrs
|
||||
* @property {PmFragment} content
|
||||
* @property {PmMark[]} marks
|
||||
* @property {String|null} text
|
||||
* @property {Number} nodeSize
|
||||
* @property {Number} childCount
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmNodeType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmMark
|
||||
* @property {PmMarkType} type
|
||||
* @property {Object} attrs
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmMarkType
|
||||
* @property {String} name
|
||||
* @property {PmSchema} schema
|
||||
* @property {PmMarkSpec} spec
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmMarkSpec
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmSchema
|
||||
* @property {PmSchema} schema
|
||||
* @property {Object<PmNodeType>} nodes
|
||||
* @property {Object<PmMarkType>} marks
|
||||
* @property {PmNodeType} topNodeType
|
||||
* @property {Object} cached
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmSelection
|
||||
* @property {PmSelectionRange[]} ranges
|
||||
* @property {PmResolvedPos} $anchor
|
||||
* @property {PmResolvedPos} $head
|
||||
* @property {Number} anchor
|
||||
* @property {Number} head
|
||||
* @property {Number} from
|
||||
* @property {Number} to
|
||||
* @property {PmResolvedPos} $from
|
||||
* @property {PmResolvedPos} $to
|
||||
* @property {Boolean} empty
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmResolvedPos
|
||||
* @property {Number} pos
|
||||
* @property {Number} depth
|
||||
* @property {Number} parentOffset
|
||||
* @property {PmNode} parent
|
||||
* @property {PmNode} doc
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmSelectionRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmTransaction
|
||||
* @property {Number} time
|
||||
* @property {PmMark[]|null} storedMarks
|
||||
* @property {PmSelection} selection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmFragment
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Function} PmCommandHandler
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmDispatchFunction} dispatch
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Function} PmDispatchFunction
|
||||
* @param {PmTransaction} tr
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} PmView
|
||||
* @param {PmEditorState} state
|
||||
* @param {Element} dom
|
||||
* @param {Boolean} editable
|
||||
* @param {Boolean} composing
|
||||
*/
|
131
resources/js/editor/util.js
Normal file
131
resources/js/editor/util.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
import schema from "./schema";
|
||||
import {DOMParser, DOMSerializer} from "prosemirror-model";
|
||||
|
||||
/**
|
||||
* @param {String} html
|
||||
* @return {PmNode}
|
||||
*/
|
||||
export function htmlToDoc(html) {
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.innerHTML = html;
|
||||
return DOMParser.fromSchema(schema).parse(renderDoc.body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmNode} doc
|
||||
* @return {string}
|
||||
*/
|
||||
export function docToHtml(doc) {
|
||||
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.appendChild(fragment);
|
||||
return renderDoc.body.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @return {String}
|
||||
*/
|
||||
export function stateToHtml(state) {
|
||||
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(state.doc.content);
|
||||
const renderDoc = document.implementation.createHTMLDocument();
|
||||
renderDoc.body.appendChild(fragment);
|
||||
return renderDoc.body.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} object
|
||||
* @return {{}}
|
||||
*/
|
||||
export function nullifyEmptyValues(object) {
|
||||
const clean = {};
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
clean[key] = (value === "") ? null : value;
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmSelection} selection
|
||||
* @param {PmMarkType} markType
|
||||
* @return {{from: Number, to: Number}}
|
||||
*/
|
||||
export function expandSelectionToMark(state, selection, markType) {
|
||||
let {from, to} = selection;
|
||||
const noRange = (from === to);
|
||||
if (noRange) {
|
||||
const markRange = markRangeAtPosition(state, markType, from);
|
||||
if (markRange.from !== -1) {
|
||||
from = markRange.from;
|
||||
to = markRange.to;
|
||||
}
|
||||
}
|
||||
return {from, to};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PmEditorState} state
|
||||
* @param {PmMarkType} markType
|
||||
* @param {Number} pos
|
||||
* @return {{from: Number, to: Number}}
|
||||
*/
|
||||
export function markRangeAtPosition(state, markType, pos) {
|
||||
const $pos = state.doc.resolve(pos);
|
||||
|
||||
const {parent, parentOffset} = $pos;
|
||||
const start = parent.childAfter(parentOffset);
|
||||
if (!start.node) return {from: -1, to: -1};
|
||||
|
||||
const mark = start.node.marks.find((mark) => mark.type === markType);
|
||||
if (!mark) return {from: -1, to: -1};
|
||||
|
||||
let startIndex = $pos.index();
|
||||
let startPos = $pos.start() + start.offset;
|
||||
let endIndex = startIndex + 1;
|
||||
let endPos = startPos + start.node.nodeSize;
|
||||
while (startIndex > 0 && mark.isInSet(parent.child(startIndex - 1).marks)) {
|
||||
startIndex -= 1;
|
||||
startPos -= parent.child(startIndex).nodeSize;
|
||||
}
|
||||
while (endIndex < parent.childCount && mark.isInSet(parent.child(endIndex).marks)) {
|
||||
endPos += parent.child(endIndex).nodeSize;
|
||||
endIndex += 1;
|
||||
}
|
||||
return {from: startPos, to: endPos};
|
||||
}
|
||||
|
||||
/**
|
||||
* @class KeyedMultiStack
|
||||
* Holds many stacks, seperated via a key, with a simple
|
||||
* interface to pop and push values to the stacks.
|
||||
*/
|
||||
export class KeyedMultiStack {
|
||||
|
||||
constructor() {
|
||||
this.stack = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @return {undefined|*}
|
||||
*/
|
||||
pop(key) {
|
||||
if (Array.isArray(this.stack[key])) {
|
||||
return this.stack[key].pop();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
*/
|
||||
push(key, value) {
|
||||
if (this.stack[key] === undefined) {
|
||||
this.stack[key] = [];
|
||||
}
|
||||
|
||||
this.stack[key].push(value);
|
||||
}
|
||||
}
|
607
resources/sass/_editor.scss
Normal file
607
resources/sass/_editor.scss
Normal file
|
@ -0,0 +1,607 @@
|
|||
|
||||
#editor.bs-editor {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
//.bs-editor .menubar {
|
||||
// border-bottom: 1px solid #DDD;
|
||||
// padding: 2px;
|
||||
//}
|
||||
//
|
||||
//.bs-editor .menuicon {
|
||||
// cursor: pointer;
|
||||
// padding: 4px;
|
||||
// min-width: 2rem;
|
||||
// border-radius: 3px;
|
||||
// border: 1px solid transparent;
|
||||
// &:hover {
|
||||
// background-color: #EEE;
|
||||
// border: 1px solid #DDD;
|
||||
// }
|
||||
//}
|
||||
|
||||
// The below originated from https://github.com/ProseMirror/prosemirror-menu
|
||||
// and is therefore subject to the MIT license found here:
|
||||
// https://github.com/ProseMirror/prosemirror-menu/blob/master/LICENSE
|
||||
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
white-space: break-spaces;
|
||||
-webkit-font-variant-ligatures: none;
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
|
||||
}
|
||||
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ProseMirror li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror table td, .ProseMirror table th {
|
||||
min-height: 1rem;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-selectednode {
|
||||
outline: 2px solid #8cf;
|
||||
}
|
||||
|
||||
/* Make sure li selections wrap around markers */
|
||||
|
||||
li.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
li.ProseMirror-selectednode:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -32px;
|
||||
right: -2px;
|
||||
top: -2px;
|
||||
bottom: -2px;
|
||||
border: 2px solid #8cf;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Protect against generic img rules */
|
||||
|
||||
img.ProseMirror-separator {
|
||||
display: inline !important;
|
||||
border: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin-right: 3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
border-right: 1px solid #ddd;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: "";
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 15;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
padding: 2px 8px 2px 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label {
|
||||
padding-inline-end: 12px;
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: "";
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 10em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover > .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active > .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
position: relative;
|
||||
min-height: 1em;
|
||||
color: #666;
|
||||
padding: 1px 6px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom: 1px solid silver;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: .8;
|
||||
vertical-align: -2px; /* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
|
||||
.ProseMirror-example-setup-style hr {
|
||||
border-top: 3px solid #FFF;
|
||||
border-bottom: 3px solid #FFF;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.ProseMirror ul, .ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt {
|
||||
background: white;
|
||||
padding: 5px 10px 5px 15px;
|
||||
border: 1px solid silver;
|
||||
position: fixed;
|
||||
border-radius: 3px;
|
||||
z-index: 11;
|
||||
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.ProseMirror-prompt h5 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 100%;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"],
|
||||
.ProseMirror-prompt textarea {
|
||||
background: #eee;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt input[type="text"] {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close {
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
color: #666;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-close:after {
|
||||
content: "✕";
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror-prompt-buttons {
|
||||
margin-top: 5px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#editor, .editor {
|
||||
background: white;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
padding: 5px 0;
|
||||
margin-bottom: 23px;
|
||||
}
|
||||
|
||||
.ProseMirror > p:first-child,
|
||||
.ProseMirror > h1:first-child,
|
||||
.ProseMirror > h2:first-child,
|
||||
.ProseMirror > h3:first-child,
|
||||
.ProseMirror > h4:first-child,
|
||||
.ProseMirror > h5:first-child,
|
||||
.ProseMirror > h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
padding: 4px 8px 4px 14px;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ProseMirror > p {
|
||||
margin-bottom: 1em
|
||||
}
|
||||
|
||||
.ProseMirror-menu-color-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-color-grid-item {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #FFF;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-table-creator-grid {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-table-creator-grid-item {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid #BBB;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-table-creator-grid-item-active {
|
||||
border: 2px solid #555;
|
||||
background-color: #DDD;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-table-creator-grid-label {
|
||||
padding: $-xs;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-wrap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-title {
|
||||
padding: $-xs $-s;
|
||||
border-bottom: 1px solid #DDD;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
margin-bottom: $-xs;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-footer {
|
||||
padding: $-xs $-s;
|
||||
border-top: 1px solid #DDD;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-top: $-xs;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-title-close {
|
||||
color: #FFF;
|
||||
position: absolute;
|
||||
top: $-xs + 2px;
|
||||
right: $-s;
|
||||
border-radius: 9px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
line-height: 0;
|
||||
vertical-align: top;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog {
|
||||
background-color: #FFF;
|
||||
border: 1px solid #DDD;
|
||||
border-radius: 3px;
|
||||
box-shadow: $bs-large;
|
||||
width: fit-content;
|
||||
min-width: 300px;
|
||||
min-height: 100px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-button {
|
||||
border: 1px solid #DDD;
|
||||
padding: $-xs $-s;
|
||||
color: #666;
|
||||
min-width: 80px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #EEE;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-button + .ProseMirror-menu-dialog-button {
|
||||
margin-left: $-xs;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
align-items: center;
|
||||
padding: $-xs 0;
|
||||
|
||||
label {
|
||||
padding: 0 $-s;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
input {
|
||||
margin: 0 $-s;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dialog-textarea-wrap {
|
||||
padding: $-xs $-s;
|
||||
|
||||
label {
|
||||
padding: 0 $-s;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-imagewrap, .ProseMirror-iframewrap {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
font-size: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-imagewrap.ProseMirror-selectednode {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.ProseMirror img[data-show-handles] {
|
||||
outline: 4px solid #000;
|
||||
}
|
||||
|
||||
.ProseMirror .ProseMirror-iframewrap iframe {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.ProseMirror-dragdummy {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
top: 0;
|
||||
max-width: none !important;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.ProseMirror-grabhandle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #000;
|
||||
z-index: 4;
|
||||
position: absolute;
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
.ProseMirror-grabhandle-left-top {
|
||||
cursor: nw-resize;
|
||||
}
|
||||
|
||||
.ProseMirror-grabhandle-right-top {
|
||||
cursor: ne-resize;
|
||||
}
|
||||
|
||||
.ProseMirror-grabhandle-right-bottom {
|
||||
cursor: se-resize;
|
||||
}
|
||||
|
||||
.ProseMirror-grabhandle-left-bottom {
|
||||
cursor: sw-resize;
|
||||
}
|
||||
|
||||
.ProseMirror .tableWrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.ProseMirror table {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ProseMirror td, .ProseMirror th {
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.ProseMirror .column-resize-handle {
|
||||
position: absolute;
|
||||
right: -2px; top: 0; bottom: 0;
|
||||
width: 4px;
|
||||
z-index: 20;
|
||||
background-color: #adf;
|
||||
pointer-events: none;
|
||||
}
|
||||
.ProseMirror.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
}
|
||||
/* Give selected cells a blue overlay */
|
||||
.ProseMirror .selectedCell:after {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
background: rgba(200, 200, 255, 0.4);
|
||||
pointer-events: none;
|
||||
}
|
|
@ -105,6 +105,9 @@ body.mce-fullscreen, body.markdown-fullscreen {
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
.align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height:auto;
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
@import "footer";
|
||||
@import "lists";
|
||||
@import "pages";
|
||||
@import "editor";
|
||||
|
||||
// Jquery Sortable Styles
|
||||
.dragged {
|
||||
|
|
83
resources/views/editor-test.blade.php
Normal file
83
resources/views/editor-test.blade.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
@extends('layouts.simple')
|
||||
|
||||
@section('body')
|
||||
<div class="container">
|
||||
|
||||
<div>
|
||||
<input id="markdown-toggle" type="checkbox">
|
||||
</div>
|
||||
|
||||
<div id="editor" class="bs-editor page-content" style="margin-bottom: 23px"></div>
|
||||
|
||||
<div id="content" style="display: none;">
|
||||
<h2>This is an editable block</h2>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, <strong>consectetur adipisicing</strong> elit. Asperiores? <br>
|
||||
Some <span style="text-decoration: underline">Underlined content</span> Lorem ipsum dolor sit amet. <br>
|
||||
Some <span style="text-decoration: line-through;">striked content</span> Lorem ipsum dolor sit amet. <br>
|
||||
Some <span style="color: red;">Red Content</span> Lorem ipsum dolor sit amet. <br>
|
||||
Some <a href="https://cats.com" target="_blank" title="link A">Linked Content</a> Lorem ipsum dolor sit amet. <br>
|
||||
</p>
|
||||
|
||||
<details>
|
||||
<summary>Dropdown here</summary>
|
||||
<h1>Inner header</h1>
|
||||
<p>Paragraph</p>
|
||||
</details>
|
||||
|
||||
<table style="width: 100%;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Header A</th>
|
||||
<th>Header B</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="width: 250px; height: 30px">Content 1</td>
|
||||
<td style="width: 320px; height: 30px">Content 2</td>
|
||||
<td style="width: 320px; height: 30px">Content 2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">Row 2, Spanning 2</td>
|
||||
<td>Row 2 spanning 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">Row 3/4, Column 1</td>
|
||||
<td>Row 3, Column 2</td>
|
||||
<td>Row 3, Column 3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Row 4, Column 2</td>
|
||||
<td>Row 4, Column 3</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{-- <iframe width="560" height="315" src="https://www.youtube.com/embed/n6hIa-fPx0M" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>--}}
|
||||
|
||||
<p><img src="/user_avatar.png" alt="Logo"></p>
|
||||
<ul>
|
||||
<li>Item A</li>
|
||||
<li>Item B</li>
|
||||
<li>Item C</li>
|
||||
</ul>
|
||||
|
||||
<p>Lorem ipsum dolor sit amet.</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<p class="align-right">Lorem ipsum dolor sit amet.</p>
|
||||
|
||||
<p class="callout info">
|
||||
This is an info callout test!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
|
||||
@section('scripts')
|
||||
<script src="{{ versioned_asset('dist/editor.js') }}" nonce="{{ $cspNonce }}"></script>
|
||||
@stop
|
|
@ -38,6 +38,8 @@ use Illuminate\View\Middleware\ShareErrorsFromSession;
|
|||
Route::get('/status', [StatusController::class, 'show']);
|
||||
Route::get('/robots.txt', [HomeController::class, 'robots']);
|
||||
|
||||
Route::view('/editor-test', 'editor-test');
|
||||
|
||||
// Authenticated routes...
|
||||
Route::middleware('auth')->group(function () {
|
||||
|
||||
|
|
Loading…
Reference in a new issue