1
0
mirror of https://github.com/chinchang/web-maker.git synced 2025-07-26 16:20:09 +02:00

Merge remote-tracking branch 'origin/master' into manifestv3

This commit is contained in:
Kushagra Gour
2024-05-06 17:54:20 +05:30
83 changed files with 10240 additions and 16784 deletions

View File

@@ -4,7 +4,7 @@
**Web-Maker** is an offline playground for your web experiments. Something like CodePen or JSFiddle, but much more faster and works offline because it runs completely on your system.
## [Open Web App](https://webmaker.app/app/) (Recommended: More features. More fun!)
## [Open Web App](https://webmaker.app/create/) (Recommended: More features. More fun!)
or

View File

@@ -8,6 +8,6 @@
- package.json
- manifest.json
- commit and tag (`git tag {version}`)
- Run `gulp buildExtension`
- Run `gulp buildExtension`. This will generate a zip - `extension-{version}.zip`
- Test out extension-{version}.zip
- If everything is good, push to master

View File

@@ -13,6 +13,7 @@ const merge = require('merge-stream');
// const zip = require('gulp-zip');
var packageJson = JSON.parse(fs.readFileSync('./package.json'));
const connect = require('gulp-connect');
const APP_FOLDER = 'create';
function minifyJs(fileName) {
const content = fs.readFileSync(fileName, 'utf8');
@@ -44,21 +45,27 @@ gulp.task('copyFiles', function () {
return merge(
gulp
.src('src/lib/codemirror/theme/*')
.pipe(gulp.dest('app/lib/codemirror/theme')),
.pipe(gulp.dest(`${APP_FOLDER}/lib/codemirror/theme`)),
gulp
.src('src/lib/codemirror/mode/**/*')
.pipe(gulp.dest('app/lib/codemirror/mode')),
gulp.src('src/lib/transpilers/*').pipe(gulp.dest('app/lib/transpilers')),
gulp.src('src/lib/prettier-worker.js').pipe(gulp.dest('app/lib/')),
gulp.src('src/lib/prettier/*').pipe(gulp.dest('app/lib/prettier')),
.pipe(gulp.dest(`${APP_FOLDER}/lib/codemirror/mode`)),
gulp
.src('src/lib/transpilers/*')
.pipe(gulp.dest(`${APP_FOLDER}/lib/transpilers`)),
gulp
.src('src/lib/prettier-worker.js')
.pipe(gulp.dest(`${APP_FOLDER}/lib/`)),
gulp
.src('src/lib/prettier/*')
.pipe(gulp.dest(`${APP_FOLDER}/lib/prettier`)),
gulp
.src(['!src/lib/monaco/monaco.bundle.js', 'src/lib/monaco/**/*'])
.pipe(gulp.dest('app/lib/monaco')),
gulp.src('src/lib/screenlog.js').pipe(gulp.dest('app/lib')),
gulp.src('icons/*').pipe(gulp.dest('app/icons')),
gulp.src('src/assets/*').pipe(gulp.dest('app/assets')),
gulp.src('src/templates/*').pipe(gulp.dest('app/templates')),
gulp.src('preview/*').pipe(gulp.dest('app/preview')),
.pipe(gulp.dest(`${APP_FOLDER}/lib/monaco`)),
gulp.src('src/lib/screenlog.js').pipe(gulp.dest(`${APP_FOLDER}/lib`)),
gulp.src('icons/*').pipe(gulp.dest(`${APP_FOLDER}/icons`)),
gulp.src('src/assets/*').pipe(gulp.dest(`${APP_FOLDER}/assets`)),
gulp.src('src/templates/*').pipe(gulp.dest(`${APP_FOLDER}/templates`)),
gulp.src('preview/*').pipe(gulp.dest(`${APP_FOLDER}/preview`)),
gulp
.src([
'src/preview.html',
@@ -68,24 +75,24 @@ gulp.task('copyFiles', function () {
'src/icon-128.png',
'src/manifest.json'
])
.pipe(gulp.dest('app')),
.pipe(gulp.dest(APP_FOLDER)),
gulp.src('build/*').pipe(gulp.dest('app')),
gulp.src('build/*').pipe(gulp.dest(APP_FOLDER)),
// Following CSS are copied to build/ folder where they'll be referenced by
// useRef plugin to concat into one.
gulp
.src('src/lib/codemirror/lib/codemirror.css')
.pipe(gulp.dest('build/lib/codemirror/lib')),
.pipe(gulp.dest(`build/lib/codemirror/lib`)),
gulp
.src('src/lib/codemirror/addon/hint/show-hint.css')
.pipe(gulp.dest('build/lib/codemirror/addon/hint')),
.pipe(gulp.dest(`build/lib/codemirror/addon/hint`)),
gulp
.src('src/lib/codemirror/addon/fold/foldgutter.css')
.pipe(gulp.dest('build/lib/codemirror/addon/fold')),
.pipe(gulp.dest(`build/lib/codemirror/addon/fold`)),
gulp
.src('src/lib/codemirror/addon/dialog/dialog.css')
.pipe(gulp.dest('build/lib/codemirror/addon/dialog')),
.pipe(gulp.dest(`build/lib/codemirror/addon/dialog`)),
gulp.src('src/lib/hint.min.css').pipe(gulp.dest('build/lib')),
gulp.src('src/lib/inlet.css').pipe(gulp.dest('build/lib')),
// gulp.src('src/style.css').pipe(gulp.dest('build')),
@@ -97,34 +104,37 @@ gulp.task('copyFiles', function () {
'src/Inconsolata.ttf',
'src/Monoid.ttf'
])
.pipe(gulp.dest('app'))
.pipe(gulp.dest(APP_FOLDER))
);
});
gulp.task('useRef', function () {
return gulp.src('build/index.html').pipe(useref()).pipe(gulp.dest('app'));
return gulp
.src('build/index.html')
.pipe(useref())
.pipe(gulp.dest(APP_FOLDER));
});
gulp.task('concatSwRegistration', function () {
const bundleFile = fs
.readdirSync('app')
.readdirSync(APP_FOLDER)
.filter(allFilesPaths => allFilesPaths.match(/bundle.*\.js$/) !== null)[0];
console.log('matched', bundleFile);
return gulp
.src(['src/service-worker-registration.js', `app/${bundleFile}`])
.src(['src/service-worker-registration.js', `${APP_FOLDER}/${bundleFile}`])
.pipe(concat(bundleFile))
.pipe(gulp.dest('app'));
.pipe(gulp.dest(APP_FOLDER));
});
gulp.task('minify', function () {
// minifyJs('app/script.js');
// minifyJs('app/vendor.js');
minifyJs('app/lib/screenlog.js');
minifyJs(`${APP_FOLDER}/lib/screenlog.js`);
return gulp
.src('app/*.css')
.src(`${APP_FOLDER}/*.css`)
.pipe(
cleanCSS(
{
@@ -137,7 +147,7 @@ gulp.task('minify', function () {
}
)
)
.pipe(gulp.dest('app'));
.pipe(gulp.dest(APP_FOLDER));
});
gulp.task('fixIndex', function (cb) {
@@ -160,7 +170,7 @@ gulp.task('fixIndex', function (cb) {
gulp.task('generate-service-worker', function (callback) {
var swPrecache = require('sw-precache');
var rootDir = 'app';
var rootDir = APP_FOLDER;
swPrecache.write(
`${rootDir}/service-worker.js`,
@@ -179,7 +189,7 @@ gulp.task('generate-service-worker', function (callback) {
gulp.task('packageExtension', function () {
child_process.execSync('rm -rf extension');
child_process.execSync('cp -R app extension');
child_process.execSync(`cp -R ${APP_FOLDER} extension`);
child_process.execSync('cp src/manifest.json extension');
child_process.execSync('cp src/options.js extension');
child_process.execSync('cp src/options.html extension');
@@ -211,12 +221,12 @@ gulp.task('buildWebsite', function () {
gulp.task('buildDistFolder', function (cb) {
child_process.execSync('rm -rf dist');
child_process.execSync('mv packages/website/_site dist');
child_process.execSync('mv app dist/');
child_process.execSync(`mv ${APP_FOLDER} dist/`);
cb();
});
gulp.task('cleanup', function () {
return child_process.exec('rm -rf build');
return child_process.exec('rm -rf build create');
});
gulp.task('start-preview-server', function () {
@@ -227,6 +237,7 @@ gulp.task('start-preview-server', function () {
});
});
// TODO: fix tasks. eg. buildWebsite isn't needed anymore
exports.release = series(
parallel('runWebpack', 'buildWebsite'),
'copyFiles',
@@ -274,7 +285,8 @@ const buildExtension = series(
'copyFiles',
'fixIndex',
'useRef',
'packageExtension'
'packageExtension',
'cleanup'
);
function runWatcher(cb) {

View File

@@ -18,10 +18,19 @@ ID = "webmaker"
# been specified, include it in the publish directory path.
publish = "dist"
# The following redirect is intended for use with most SPAs that handle
# routing internally.
[[redirects]]
from = "https://preview.webmaker.app/*"
to = "/app/preview/:splat"
to = "/create/preview/:splat"
status = 200
force = true
force = true
[[redirects]]
from = "https://preview.v6--webmaker.netlify.app/*"
to = "/create/preview/:splat"
status = 200
force = true
[[redirects]]
from = "/create/*"
to = "/create/index.html"
status = 200

20867
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
{
"name": "web-maker",
"version": "5.2.0",
"version": "6.1.0",
"description": "A blazing fast & offline web playground",
"scripts": {
"start": "concurrently --kill-others \"gulp start-preview-server\" \"npm run -s dev\"",
"build": "preact build --template src/index.html --prerender false --no-inline-css --sw false --esm false",
"dev": "preact watch --template src/index.html",
"build": "preact build --template src/index.ejs --prerender false --no-inline-css --sw false",
"dev": "preact watch --template src/index.ejs",
"serve-website": "cd packages/website; npm start",
"build-website": "cd packages/website; npm run build",
"lint": "eslint src",
@@ -65,24 +65,25 @@
"markdown-it": "^8.4.2",
"markdown-it-anchor": "^5.0.2",
"merge-stream": "^1.0.1",
"preact-cli": "^3.0.0",
"preact-cli": "^4.0.0-next.6",
"sw-precache": "^5.2.0"
},
"dependencies": {
"@emmetio/codemirror-plugin": "^0.5.4",
"@lingui/react": "^2.8.3",
"canvas-confetti": "^1.9.2",
"code-blast-codemirror": "chinchang/code-blast-codemirror#web-maker",
"codemirror": "^5.37.0",
"codemirror": "^5.65.16",
"copy-webpack-plugin": "^4.5.1",
"esprima": "^4.0.0",
"firebase": "^10.8.0",
"jszip": "^3.1.5",
"preact": "^10.5.13",
"preact": "^10.17.0",
"preact-portal": "^1.1.3",
"preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1",
"prettier": "^2.2.1",
"react-inspector": "^2.3.0",
"prettier": "^3.0.2",
"react-inspector": "^6.0.2",
"split.js": "^1.5.11"
},
"engines": {

View File

@@ -7,8 +7,11 @@ excludeFromSitemap: true
<div class="ta-c">
<figure>
<img src="/images/404.png" style="width:65vh" />
<figcaption>Image by https://icons8.com</figcaption>
<img src="/images/404.png" style="width: 65vh" />
<figcaption style="opacity: 0.5">Image by https://icons8.com</figcaption>
</figure>
<p>Uh oh, the page you wanted to see isn't here. How about <a href="/">going to the homepage</a>?</p>
<p style="margin-top: 2rem">
Uh oh, the page you wanted to see isn't here. How about
<a href="/">going to the homepage</a>?
</p>
</div>

View File

@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
@@ -114,7 +114,7 @@
padding: 1rem;
max-width: var(--layout-max-width);
margin: 0 auto;
min-height: 55vh;
min-height: calc(100dvh - 4rem);
}
@media screen and (max-width: 700px) {
@@ -514,7 +514,7 @@
A blazing fast & offline frontend playground in your browser
</h2>
<div style="margin-top: 30px" id="cta" class="mb-2">
<a class="btn download-btn web-app-btn" href="/app/">
<a class="btn download-btn web-app-btn" href="/create/">
<span>Open Web App</span>
</a>
<p style="margin-top: 3px">

View File

@@ -8,7 +8,7 @@ Web Maker is available as a Web app and a Chrome extension. Web app is recommend
**Web app**
The web app requires no installation. Just [goto the app url](/app/). Once you open the app, it is available for offline use in future.
The web app requires no installation. Just [goto the app url](/create/). Once you open the app, it is available for offline use in future.
**Chrome extension**
@@ -20,7 +20,7 @@ Here is the link to the Chrome extension 👉 [Chrome Web Store](https://chrome.
**Web app**
As mentioned, the Web app can be simply opened by visiting [https://webmaker.app/app/](/app/).
As mentioned, the Web app can be simply opened by visiting [https://webmaker.app/create/](/create/).
**Chrome extension**

View File

@@ -9,13 +9,13 @@ At rare times, you might find yourself in a situation where you are not able to
Say, you want to turn off `autoPreview` (which runs your code automatically on fresh launch and then on subsequent changes). You can do so by opening the following URL:
```
https://webmaker.app/app?settings=autoPreview:false
https://webmaker.app/create?settings=autoPreview:false
```
Say, you want to turn off monaco editor also:
```
https://webmaker.app/app?settings=autoPreview:false,isMonacoEditorOn:false
https://webmaker.app/create?settings=autoPreview:false,isMonacoEditorOn:false
```
**Note**: Currently overrides work only for `autoPreview` and `isMonacoEditorOn` options. More coming soon.

View File

@@ -1,5 +1,3 @@
// var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
/**
* Function that mutates original webpack config.
* Supports asynchronous changes when promise is returned.
@@ -21,9 +19,9 @@ export default function (config, env, helpers) {
htmlWebpackPlugin.plugin.options.favicon = false;
// Required for lingui-macros
let { rule } = helpers.getLoadersByName(config, 'babel-loader')[0];
let babelConfig = rule.options;
babelConfig.plugins.push('macros');
// let { rule } = helpers.getLoadersByName(config, 'babel-loader')[0];
// let babelConfig = rule.options;
// babelConfig.plugins.push('macros');
if (env.isProd) {
config.devtool = false; // disable sourcemaps
@@ -33,25 +31,5 @@ export default function (config, env, helpers) {
// Remove the default hash append in chunk name
config.output.chunkFilename = '[name].chunk.js';
// config.plugins.push(
// new CommonsChunkPlugin({
// name: 'vendor',
// minChunks: ({ resource }) => /node_modules/.test(resource)
// })
// );
const swPlugin = helpers.getPluginsByName(
config,
'SWPrecacheWebpackPlugin'
)[0];
if (swPlugin) {
// config.plugins.splice(swPlugin.index, 1);
}
const uglifyPlugin = helpers.getPluginsByName(config, 'UglifyJsPlugin')[0];
if (uglifyPlugin) {
// config.plugins.splice(uglifyPlugin.index, 1);
}
}
}

View File

@@ -0,0 +1,20 @@
window.addEventListener('message', e => {
// Recieving from app window
if (e.data && e.data.contents && e.data.contents.match(/<html/)) {
const frame = document.querySelector('iframe');
frame.src = frame.src;
setTimeout(() => {
frame.contentDocument.open();
frame.contentDocument.write(e.data.contents);
frame.contentDocument.close();
}, 10);
}
if (e.data && e.data.url && e.data.url.match(/index\.html/)) {
document.querySelector('iframe').src = e.data.url;
}
// Recieving from preview iframe
if (e.data && e.data.logs) {
window.opener.postMessage(e.data, '*');
}
});

22
preview/preview.html Normal file
View File

@@ -0,0 +1,22 @@
<style>
body {
margin: 0;
}
#demo-frame {
border: 0;
width: 100%;
background: white;
height: 100%;
}
</style>
<body>
<iframe
src="about://blank"
frameborder="0"
id="demo-frame"
allowfullscreen
></iframe>
<script src="detached-window.js"></script>
</body>

View File

@@ -3,58 +3,59 @@
import CodeMirror from 'codemirror';
// Make CodeMirror available globally so the modes' can register themselves.
window.CodeMirror = CodeMirror
window.CodeMirror = CodeMirror;
if (!CodeMirror.modeURL) CodeMirror.modeURL = 'lib/codemirror/mode/%N/%N.js';
if (!CodeMirror.modeURL) CodeMirror.modeURL = '/lib/codemirror/mode/%N/%N.js';
var loading = {}
var loading = {};
function splitCallback(cont, n) {
var countDown = n
var countDown = n;
return function () {
if (--countDown === 0) cont()
}
if (--countDown === 0) cont();
};
}
function ensureDeps(mode, cont) {
var deps = CodeMirror.modes[mode].dependencies
if (!deps) return cont()
var missing = []
var deps = CodeMirror.modes[mode].dependencies;
if (!deps) return cont();
var missing = [];
for (var i = 0; i < deps.length; ++i) {
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i])
if (!CodeMirror.modes.hasOwnProperty(deps[i])) missing.push(deps[i]);
}
if (!missing.length) return cont()
var split = splitCallback(cont, missing.length)
for (i = 0; i < missing.length; ++i) CodeMirror.requireMode(missing[i], split)
if (!missing.length) return cont();
var split = splitCallback(cont, missing.length);
for (i = 0; i < missing.length; ++i)
CodeMirror.requireMode(missing[i], split);
}
CodeMirror.requireMode = function (mode, cont) {
if (typeof mode !== 'string') mode = mode.name
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont)
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont)
if (typeof mode !== 'string') mode = mode.name;
if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont);
if (loading.hasOwnProperty(mode)) return loading[mode].push(cont);
var file = CodeMirror.modeURL.replace(/%N/g, mode)
var file = CodeMirror.modeURL.replace(/%N/g, mode);
var script = document.createElement('script')
script.src = file
var others = document.getElementsByTagName('script')[0]
var list = loading[mode] = [cont]
var script = document.createElement('script');
script.src = file;
var others = document.getElementsByTagName('script')[0];
var list = (loading[mode] = [cont]);
CodeMirror.on(script, 'load', function () {
ensureDeps(mode, function () {
for (var i = 0; i < list.length; ++i) list[i]()
})
})
for (var i = 0; i < list.length; ++i) list[i]();
});
});
others.parentNode.insertBefore(script, others)
}
others.parentNode.insertBefore(script, others);
};
CodeMirror.autoLoadMode = function (instance, mode) {
if (CodeMirror.modes.hasOwnProperty(mode)) return
if (CodeMirror.modes.hasOwnProperty(mode)) return;
CodeMirror.requireMode(mode, function () {
instance.setOption('mode', instance.getOption('mode'))
})
}
instance.setOption('mode', instance.getOption('mode'));
});
};
export default CodeMirror
export default CodeMirror;

View File

@@ -10,6 +10,12 @@ export function trackEvent(category, action, label, value) {
}
if (window.ga) {
ga('send', 'event', category, action, label, value);
fetch('https://event-ajhkrtmkaq-uc.a.run.app', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category, action, label, value })
});
}
}

BIN
src/assets/kaboom-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

BIN
src/assets/pro-panda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="128" height="128" viewBox="0 0 128 128" xml:space="preserve">
<desc>Created with Fabric.js 5.2.4</desc>
<defs>
</defs>
<rect x="0" y="0" width="100%" height="100%" fill="transparent"></rect>
<g transform="matrix(1 0 0 1 64 64)" id="f3d7db2e-8d04-4ee3-99c9-b0befc232687" >
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1; visibility: hidden;" vector-effect="non-scaling-stroke" x="-64" y="-64" rx="0" ry="0" width="128" height="128" />
</g>
<g transform="matrix(Infinity NaN NaN Infinity 0 0)" id="3dd1926a-5f11-4f73-a873-48f6092f16b5" >
</g>
<g transform="matrix(2.54 0 0 2.54 63.96 65.38)" >
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(56,189,248); fill-rule: evenodd; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-25.52, -15.22)" d="M 25.517 0 C 18.712 0 14.46 3.382 12.758 10.146 C 15.309999999999999 6.764000000000001 18.287 5.496 21.689 6.341000000000001 C 23.63 6.823000000000001 25.018 8.223 26.553 9.773000000000001 C 29.055 12.297 31.951 15.218000000000002 38.275 15.218000000000002 C 45.079 15.218000000000002 49.332 11.836000000000002 51.033 5.073000000000002 C 48.482 8.455000000000002 45.505 9.723000000000003 42.103 8.877000000000002 C 40.161 8.395000000000003 38.773 6.995000000000003 37.238 5.446000000000002 C 34.736 2.92 31.841 0 25.517 0 z M 12.758 15.218 C 5.954 15.218 1.701 18.6 0 25.364 C 2.552 21.982 5.529 20.714 8.93 21.559 C 10.872 22.041 12.26 23.441000000000003 13.795 24.991 C 16.297 27.515 19.192 30.436 25.517 30.436 C 32.321 30.436 36.574 27.055 38.275 20.291 C 35.723 23.673000000000002 32.745999999999995 24.941000000000003 29.344 24.096 C 27.403000000000002 23.613 26.015 22.213 24.48 20.664 C 21.978 18.14 19.082 15.218000000000002 12.758000000000001 15.218000000000002 z" stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

357
src/components/Assets.jsx Normal file
View File

@@ -0,0 +1,357 @@
import React, { useState, useEffect } from 'react';
import firebase from 'firebase/app';
import 'firebase/storage';
import { HStack, Stack, VStack } from './Stack';
import { copyToClipboard } from '../utils';
import { Trans } from '@lingui/macro';
import { ProBadge } from './ProBadge';
import { LoaderWithText } from './Loader';
import { Text } from './Text';
import { Icon } from './Icons';
function getFileType(url) {
// get extension from a url using URL API
const ext = new URL(url).pathname.split('.').pop();
if (['jpg', 'jpeg', 'png', 'gif', 'svg'].includes(ext)) {
return 'image';
}
return ext;
}
const Assets = ({ onProBtnClick, onLoginBtnClick }) => {
const [files, setFiles] = useState([]);
const [isFetchingFiles, setIsFetchingFiles] = useState(false);
const [searchTerm, setSearchTerm] = useState('');
const [filteredFiles, setFilteredFiles] = useState([]);
const [isUploading, setIsUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState();
const [listType, setListType] = useState('grid');
const storageRef = firebase.storage().ref(`assets/${window.user?.uid}`);
const uploadFile = file => {
if (file.size > 1024 * 1024) {
// 1MB limit
alert('File size must be less than 1MB');
return;
}
setIsUploading(true);
const metadata = {
cacheControl: 'public, max-age=3600' // 1 hr
};
const fileRef = storageRef.child(file.name);
const task = fileRef.put(file, metadata);
task.on(
'state_changed',
snapshot => {
// Observe state change events such as progress, pause, and resume
// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
},
error => {
// Handle unsuccessful uploads
setIsUploading(false);
console.error('File upload error:', error);
alertsService.add('⚠️ File upload failed');
},
() => {
// uploadTask.snapshot.ref.getDownloadURL().then((downloadURL) => {
// console.log('File available at', downloadURL);
// });
alertsService.add('File uploaded successfully');
fetchFiles();
setIsUploading(false);
}
);
};
// Function to handle file upload
const handleFileUpload = e => {
const file = e.target.files[0];
uploadFile(file);
};
// Function to fetch existing files
const fetchFiles = () => {
setIsFetchingFiles(true);
storageRef
.listAll()
.then(result => {
const filePromises = result.items.map(item => {
return item.getDownloadURL().then(url => {
return { name: item.name, url };
});
});
Promise.all(filePromises).then(files => {
files.forEach(f => (f.ext = getFileType(f.url)));
setFiles(files);
});
setIsFetchingFiles(false);
})
.catch(error => {
console.error('File fetch error:', error);
setIsFetchingFiles(false);
});
};
// Function to handle search input change
const handleSearchChange = e => {
const term = e.target.value;
setSearchTerm(term);
};
useEffect(() => {
if (window.user?.isPro) {
fetchFiles();
}
}, []);
useEffect(() => {
if (searchTerm) {
setFilteredFiles(
files.filter(file =>
file.name.toLowerCase().includes(searchTerm.toLowerCase())
)
);
} else {
setFilteredFiles(files);
}
}, [files, searchTerm]);
const [isDropTarget, setIsDropTarget] = useState(false);
const handleDragDropEvent = e => {
if (e.type === 'dragover') {
// required for drop to work
e.preventDefault();
} else if (e.type === 'dragleave') {
e.preventDefault();
// so that individual nested elements don't trigger dragleave
if (e.currentTarget.contains(e.target)) return;
setIsDropTarget(false);
} else if (e.type === 'dragenter') {
setIsDropTarget(true);
}
};
const handleDrop = e => {
e.preventDefault();
setIsDropTarget(false);
if (e.dataTransfer.items) {
const file = e.dataTransfer.items[0].getAsFile();
uploadFile(file);
}
};
const [lastCopiedFile, setLastCopiedFile] = useState({ name: '', count: 0 });
const copyFileUrl = url => {
let copyContent = url;
if (lastCopiedFile.name === url) {
lastCopiedFile.count = (lastCopiedFile.count + 1) % 3;
} else {
lastCopiedFile.count = 0;
lastCopiedFile.name = url;
}
switch (lastCopiedFile.count) {
case 0:
copyContent = url;
break;
case 1:
copyContent = `<img src="${url}" />`;
break;
case 2:
copyContent = `url("${url}")`;
break;
}
setLastCopiedFile({ ...lastCopiedFile });
copyToClipboard(copyContent).then(() => {
switch (lastCopiedFile.count) {
case 0:
alertsService.add('File URL copied');
break;
case 1:
alertsService.add('File URL copied as <IMG> tag');
break;
case 2:
alertsService.add('File URL copied as CSS image URL');
break;
}
});
};
const removeFileHandler = index => {
const file = files[index];
const answer = confirm(`Are you sure you want to delete "${file.name}"?`);
if (!answer) return;
const fileRef = storageRef.child(file.name);
fileRef
.delete()
.then(() => {
alertsService.add('File deleted successfully');
setFiles(files.filter((_, i) => i !== index));
})
.catch(error => {
console.error('File delete error:', error);
});
};
if (!window.user?.isPro) {
return (
<VStack align="stretch" gap={2}>
<p>Assets feature is available in PRO plan.</p>
<button
class="btn btn--pro"
onClick={window.user ? onProBtnClick : onLoginBtnClick}
>
<HStack gap={1} fullWidth justify="center">
{window.user ? <>Upgrade to PRO</> : <>Login & upgrade to PRO</>}
</HStack>
</button>
</VStack>
);
}
return (
<div
onDragEnter={handleDragDropEvent}
onDragLeave={handleDragDropEvent}
onDragOver={handleDragDropEvent}
onDrop={handleDrop}
>
<HStack gap={1} align="center">
<h1>
<Trans>Assets</Trans>
</h1>
<ProBadge />
</HStack>
<div
class="asset-manager__upload-box"
style={{
background: isDropTarget ? '#19a61940' : 'transparent',
borderColor: isDropTarget ? 'limegreen' : null
}}
>
{isUploading ? <div class="asset-manager__progress-bar"></div> : null}
<div style={{ visibility: isUploading ? 'hidden' : 'visible' }}>
<VStack gap={1} align="stretch">
<label style="background: #00000001">
<Text tag="p" align="center">
Drop files or click here to upload
</Text>
<Text tag="p" appearance="secondary" align="center">
File should be max 300KB in size
</Text>
<input
type="file"
onChange={handleFileUpload}
style={{ marginTop: 'auto', display: 'none' }}
/>
</label>
</VStack>
</div>
</div>
{isFetchingFiles && <LoaderWithText>Fetching files...</LoaderWithText>}
<VStack align="stretch" gap={1}>
{files.length ? (
<Stack gap={1}>
<input
type="text"
placeholder="Search files"
value={searchTerm}
onChange={handleSearchChange}
style={{ width: '100%' }}
/>
<button
class={`btn btn--dark ${
listType === 'list' ? 'btn--active' : ''
} hint--rounded hint--top-left`}
onClick={() => setListType('list')}
aria-label="List view"
>
<Icon name="view-list" />
</button>
<button
class={`btn btn--dark ${
listType === 'grid' ? 'btn--active' : ''
} hint--rounded hint--top-left`}
onClick={() => setListType('grid')}
aria-label="Grid view"
>
<Icon name="view-grid" />
</button>
</Stack>
) : null}
<div
class={`asset-manager__file-container ${
listType === 'grid' ? 'asset-manager__file-container--grid' : ''
}`}
>
{filteredFiles.map((file, index) => (
<div
key={index}
class={`asset-manager__file ${
listType === 'grid' ? 'asset-manager__file--grid' : ''
}`}
>
{/* <a href={file.url} target="_blank" rel="noopener noreferrer"> */}
{file.ext === 'image' ? (
<img src={file.url} class="asset-manager__file-image" />
) : (
<div
style={{
position: 'relative',
display: 'flex'
}}
class="asset-manager__file-image"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="#ffffff33"
viewBox="0 0 24 24"
>
<path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z" />
</svg>
<span className="asset-manager__file-ext">{file.ext}</span>
</div>
)}
<div class="asset-manager__file-actions">
<Stack gap={1} fullWidth justify="center">
<button
class={`btn btn--dark ${
listType === 'list' ? 'btn--active' : ''
} hint--rounded hint--top hint--medium`}
onClick={() => copyFileUrl(file.url)}
aria-label="Copy URL (or keep clicking to copy other formats)"
>
<Icon name="copy" />
</button>
<button
class={`btn btn--dark ${
listType === 'list' ? 'btn--active' : ''
} hint--rounded hint--top-left`}
onClick={() => removeFileHandler(index)}
aria-label="Delete"
>
<Icon name="trash" />
</button>
</Stack>
</div>
<span class="asset-manager__file-name">{file.name}</span>
{/* </a> */}
</div>
))}
</div>
</VStack>
</div>
);
};
export { Assets };

View File

@@ -74,46 +74,94 @@ function Notification({ version, isLatest, ...props }) {
</a>
</p>
<p>
Web Maker now has more than 50K weekly active users! Thank you for
Web Maker now has more than 70K weekly active users! Thank you for
being a part of this community of awesome developers. If you find
Web Maker helpful,{' '}
<a
href="https://chrome.google.com/webstore/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
href="https://chromewebstore.google.com/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
target="_blank"
rel="noopener noreferrer"
class="btn"
>
Please rate Web Maker <span class="star" />
Please rate Web Maker &nbsp;
<span class="star" />
</a>
&nbsp;
<a
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,editor,chrome,extension"
class="btn"
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline frontend playground! via @webmakerApp&related=webmakerApp&hashtags=web,editor,chrome,extension"
target="_blank"
rel="noopener noreferrer"
class="btn"
>
Share it
</a>
&nbsp;
<Button
aria-label="Support the developer"
onClick={props.onSupportBtnClick}
data-event-action="supportDeveloperChangelogBtnClick"
data-event-category="ui"
class="btn btn-icon"
>
Support the developer
</Button>
</p>
</div>
) : null}
</div>
);
}
export function Notifications(props) {
export function Changelog(props) {
return (
<div>
<h1>Whats new?</h1>
<Notification version="6.1.0" {...props} isLatest={true}>
<li>
<strong>🕹 Kaboom game engine</strong>: Kaboom.js is now available in
quick-add library list. Also, a new template to quick-start in Kaboom
has been added.
</li>
<NotificationItem type="bug">
Incorrect/old preview rendering bug is fixed.
</NotificationItem>
</Notification>
<Notification version="6.0.0" {...props}>
<li>
<strong>🎁 PRO plan 🎉</strong>: Today I introduce to you - Web
Maker's PRO plan! A set of additional super-features which you can
buy. The PRO plan is available as monthly/annual subscription as well
as a one-time lifetime price! Let's see what you get as a PRO.
</li>
<li>
<strong>🔓 Share your creations</strong>: Web Maker has always been a
privacy-first app. Continuing that culture, today we introduce "Share
your creation" feature. Your creations are still created as private
but now you can securely make them public to share with the world. As
a free user you can have 1 creation public at a time. Upgrading to PRO
gives you unlimited public creations.
</li>
<li>
<strong>🗄 Asset hosting</strong>: No more going to other places in
order to host your images, CSS or JS files. Web Maker PRO gives you
the ability to host your assets right inside Web Maker. You can upload
images, CSS and JS files and use them in your creations.
</li>
<li>
<strong>📁 Files mode</strong>: As a free user you could always create
2 creations in Files mode. With PRO, you can create unlimited
creations in Files mode.
</li>
<NotificationItem type="ui">
Web app is now available on webmaker.app/create (Previously available
on webmaker.app/app)
</NotificationItem>
<NotificationItem type="ui">
Fork button is now available in the header too
</NotificationItem>
</Notification>
<Notification version="5.3.0" {...props} isLatest={true}>
<li>
<strong>Tailwind CSS templates</strong>: Tailwind CSS template is now
available! If you are on the Chrome extension, use the Tailwind CSS 2
template which is old but works without JavaScript (since JavaScript
is disabled currently in Chrome extension). That said, JavaScript is
coming back soon to the Chrome extension! 😃
</li>
<li> Popular libraries updated to latest versions.</li>
</Notification>
<Notification version="5.2.0" {...props}>
<li>
<strong>Improvement</strong>: Atomic CSS (Atomizer) has been updated
@@ -157,7 +205,7 @@ export function Notifications(props) {
Add library feature is fixed.
</NotificationItem>
</Notification>
<Notification version="5.0.0" {...props} isLatest={true}>
<Notification version="5.0.0" {...props}>
<li>
<strong>We are Back! 😎</strong>: After almost 2 years of nothing
being shipped, we are back in action. That too with a bang! We have a
@@ -192,7 +240,7 @@ export function Notifications(props) {
<strong>autoPreview</strong> and <strong>isMonacoEditorOn</strong> are
supported. Example:{' '}
<code>
https://webmaker.app/app?settings=autoPreview:false,isMonacoEditorOn:true
https://webmaker.app/create?settings=autoPreview:false,isMonacoEditorOn:true
</code>
</li>
<NotificationItem type="bug">
@@ -585,11 +633,11 @@ export function Notifications(props) {
as web app that runs offline just like the extension! Checkout it
out -&gt;
<a
href="https://webmaker.app/app/"
href="https://webmaker.app/create/"
target="_blank"
rel="noopener noreferrer"
>
https://webmaker.app/app/
https://webmaker.app/create/
</a>
.
</li>

View File

@@ -237,7 +237,7 @@ export default class CodeEditor extends Component {
monacoDepsDeferred = deferred();
loadCss({ url: 'lib/monaco/monaco.css', id: 'monaco-css' });
import(
/* webpackChunkName: "monaco" */ '../lib/monaco/monaco.bundle.js'
/* webpackChunkName: "monaco" */ '/lib/monaco/monaco.bundle.js'
).then(() => {
monacoDepsDeferred.resolve();
});

View File

@@ -12,15 +12,12 @@ class LogRow extends Component {
const theme = {
...chromeDark,
...{
OBJECT_VALUE_STRING_COLOR: 'green',
BASE_FONT_SIZE: '20px',
TREENODE_FONT_SIZE: '20px'
}
};
return (
<Inspector theme={theme} theme="chromeDark" data={this.props.data} />
);
return <Inspector theme={theme} data={this.props.data} />;
}
}

View File

@@ -21,6 +21,12 @@ const minCodeWrapSize = 33;
/* global htmlCodeEl
*/
const PREVIEW_FRAME_HOST = window.DEBUG
? 'http://localhost:7888'
: `https://wbmakr.com`;
let cachedSandboxAttribute = '';
export default class ContentWrap extends Component {
constructor(props) {
super(props);
@@ -152,29 +158,56 @@ export default class ContentWrap extends Component {
}
if (shouldInlineJs) {
const writeInsideIframe = () => {
if (this.detachedWindow) {
log('✉️ Sending message to detached window');
this.detachedWindow.postMessage({ contents }, '*');
} else if (window.IS_EXTENSION) {
if (this.detachedWindow) {
log('✉️ Sending message to detached window');
this.detachedWindow.postMessage({ contents }, '*');
} else {
// 1. we refresh the frame so that all JS is cleared in the frame. this will
// break the iframe since sandboxed frame isn't served by SW (needed for offline support)
// 2. we cache and remove the sandbox attribute and refresh again so that it gets served by SW
// 3. we add back cached sandbox attr & write the contents to the iframe
const refreshAndDo = fn => {
Promise.race([
// Just in case onload promise doesn't resolves
new Promise(resolve => {
setTimeout(resolve, 400);
}),
new Promise(resolve => {
this.frame.onload = resolve;
})
]).then(fn);
// Setting to blank string cause frame to reload
// if (window.IS_EXTENSION) {
// this.frame.src = '';
// } else {
this.frame.src = this.frame.src;
// }
};
const writeInsideIframe = () => {
if (!cachedSandboxAttribute && window.DEBUG) {
// alert('sandbox empty');
}
// console.log('setting back sandbox attr', sandbox);
// this.frame.setAttribute('sandbox', cachedSandboxAttribute);
// this.frame.removeAttribute('sweet');
// if (window.IS_EXTENSION) {
// this.frame.contentDocument.open();
// this.frame.contentDocument.write(contents);
// this.frame.contentDocument.close();
// } else {
this.frame.contentWindow.postMessage({ contents }, '*');
} else {
this.frame.contentDocument.open();
this.frame.contentDocument.write(contents);
this.frame.contentDocument.close();
}
};
Promise.race([
// Just in case onload promise doesn't resolves
new Promise(resolve => {
setTimeout(resolve, 200);
}),
new Promise(resolve => {
this.frame.onload = resolve;
})
]).then(writeInsideIframe);
// Setting to blank string cause frame to reload
this.frame.src = this.frame.src;
// }
};
// refreshAndDo(() => {
// cachedSandboxAttribute = this.frame.getAttribute('sandbox');
// // console.log('removing sandbox', sandbox);
// // this.frame.setAttribute('sweet', sandbox);
// // this.frame.removeAttribute('sandbox');
// refreshAndDo(writeInsideIframe);
// });
refreshAndDo(writeInsideIframe);
}
} else {
// DEPRECATED
// we need to store user script in external JS file to prevent inline-script
@@ -240,7 +273,8 @@ export default class ContentWrap extends Component {
false &&
!isForced &&
currentCode.html === this.codeInPreview.html &&
currentCode.js === this.codeInPreview.js
currentCode.js === this.codeInPreview.js &&
false
) {
computeCss(
cssMode === CssModes.ACSS ? currentCode.html : currentCode.css,
@@ -326,7 +360,7 @@ export default class ContentWrap extends Component {
// Replace correct css file in LINK tags's href
if (prefs.editorTheme) {
window.editorThemeLinkTag.href = `lib/codemirror/theme/${prefs.editorTheme}.css`;
window.editorThemeLinkTag.href = `./lib/codemirror/theme/${prefs.editorTheme}.css`;
}
window.fontStyleTag.textContent =
@@ -517,11 +551,20 @@ export default class ContentWrap extends Component {
const iframeHeight = iframeBounds.height;
document.body.classList.add('is-detached-mode');
this.detachedWindow = window.open(
'./preview.html',
'Web Maker',
`width=${iframeWidth},height=${iframeHeight},resizable,scrollbars=yes,status=1`
);
if (window.IS_EXTENSION) {
this.detachedWindow = window.open(
'./preview.html',
'Web Maker',
`width=${iframeWidth},height=${iframeHeight},resizable,scrollbars=yes,status=1`
);
} else {
this.detachedWindow = window.open(
`${PREVIEW_FRAME_HOST}/preview.html`,
'Web Maker',
`width=${iframeWidth},height=${iframeHeight},resizable,scrollbars=yes,status=1`
);
}
// Trigger initial render in detached window
setTimeout(() => {
this.setPreviewContent(true);
@@ -837,7 +880,7 @@ export default class ContentWrap extends Component {
<a
class="code-wrap__header-btn "
title="Format code"
onClick={this.prettifyBtnClickHandler.bind(this, 'css')}
onClick={this.prettifyBtnClickHandler.bind(this, 'js')}
>
<svg>
<use xlinkHref="#code-brace-icon" />
@@ -878,13 +921,26 @@ export default class ContentWrap extends Component {
</div>
</SplitPane>
<div class="demo-side" id="js-demo-side" style="">
<iframe
ref={el => (this.frame = el)}
frameborder="0"
id="demo-frame"
allowfullscreen
src="./indexpm.html"
/>
{window.IS_EXTENSION ? (
<iframe
ref={el => (this.frame = el)}
frameborder="0"
id="demo-frame"
src="./indexpm.html"
allowfullscreen="true"
/>
) : (
<iframe
src={`./indexpm.html`}
ref={el => (this.frame = el)}
frameborder="0"
id="demo-frame"
sandbox="allow-downloads allow-forms allow-modals allow-pointer-lock allow-popups allow-presentation allow-scripts allow-top-navigation-by-user-activation"
allow="accelerometer; camera; encrypted-media; display-capture; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write; web-share"
allowpaymentrequest="true"
allowfullscreen="true"
/>
)}
<PreviewDimension ref={comp => (this.previewDimension = comp)} />

View File

@@ -29,7 +29,7 @@ import { FileIcon } from './FileIcon';
const minCodeWrapSize = 33;
const PREVIEW_FRAME_HOST = window.DEBUG
? 'http://localhost:7888'
: `https://preview.${location.host}`;
: `https://wbmakr.com`;
/* global htmlCodeEl
*/
@@ -355,7 +355,7 @@ export default class ContentWrapFiles extends Component {
// Replace correct css file in LINK tags's href
if (prefs.editorTheme) {
window.editorThemeLinkTag.href = `lib/codemirror/theme/${prefs.editorTheme}.css`;
window.editorThemeLinkTag.href = `./lib/codemirror/theme/${prefs.editorTheme}.css`;
}
window.fontStyleTag.textContent =

View File

@@ -5,6 +5,7 @@ import templates from '../templateList';
import { BetaTag } from './common';
import { trackEvent } from '../analytics';
import Tabs, { TabPanel } from './Tabs';
import { ProBadge } from './ProBadge';
export class CreateNewModal extends Component {
constructor(props) {
@@ -150,11 +151,10 @@ export class CreateNewModal extends Component {
<h1 class="mt-0">Create New</h1>
<Tabs horizontal onChange={this.modeChangeHandler}>
<TabPanel label={option1}>
<div class="d-f fxw-w">
<div class="templates-container">
<button
type="button"
class="btn btn--primary"
style="margin:20px 10px"
onClick={() => {
trackEvent('ui', 'startBlankBtnClick');
onBlankTemplateSelect();
@@ -170,17 +170,17 @@ export class CreateNewModal extends Component {
item={template}
focusable
onClick={onTemplateSelect.bind(null, template, false)}
hasOptions={false}
/>
);
})}
</div>
</TabPanel>
<TabPanel label={option2}>
<div class="d-f fxw-w show-when-app">
<div class="templates-container show-when-app">
<button
type="button"
class="btn btn--primary"
style="margin:20px 10px"
onClick={() => {
trackEvent('ui', 'startBlankFileBtnClick');
onBlankFileTemplateSelect();
@@ -196,14 +196,19 @@ export class CreateNewModal extends Component {
item={template}
focusable
onClick={onTemplateSelect.bind(null, template, true)}
hasOptions={false}
/>
);
}
})}
</div>
<p>
2 files mode creations available in Free plan. To create unlimited
files mode creations, upgrade to <ProBadge />.
</p>
<div class="show-when-extension">
Files modes is currently only available in Web app.{' '}
<a href="https://webmaker.app/app/">Try the Web app now</a>.
<a href="https://webmaker.app/create/">Try the Web app now</a>.
</div>
</TabPanel>
</Tabs>

View File

@@ -1,337 +1,342 @@
import { h, Component } from 'preact';
import { Button } from './common';
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { ProBadge } from './ProBadge';
import { HStack } from './Stack';
import { useEffect, useState } from 'preact/hooks';
class JS13K extends Component {
constructor(props) {
super(props);
const compoDate = new Date('August 13 2018 11:00 GMT');
var now = new Date();
var daysLeft;
const JS13K = props => {
const [daysLeft, setDaysLeft] = useState(0);
useEffect(() => {
const compoDate = new Date('August 13 2024 11:00 GMT');
const now = new Date();
if (+compoDate > +now) {
daysLeft = Math.floor((compoDate - now) / 1000 / 3600 / 24);
const _daysLeft = Math.floor((compoDate - now) / 1000 / 3600 / 24);
setDaysLeft(_daysLeft);
}
this.setState({
daysLeft
});
}
}, []);
render() {
const codeSizeInKb = this.props.codeSize
? (this.props.codeSize / 1024).toFixed(2)
: 0;
return (
const codeSizeInKb = props.codeSize ? (props.codeSize / 1024).toFixed(2) : 0;
return (
<div
role="button"
className="flex flex-v-center"
tabIndex="0"
onClick={props.onClick}
onBlur={props.onBlur}
>
<img src="assets/js13kgames.png" alt="JS13K Games logo" height="24" />{' '}
<div className="footer__js13k-days-left">{daysLeft} days to go</div>
<div
role="button"
class="flex flex-v-center"
tabIndex="0"
onClick={this.props.onClick}
onBlur={this.props.onBlur}
className="footer__js13k-code-size"
style={{
color: codeSizeInKb > 10 ? 'crimson' : 'limegreen'
}}
>
<img src="assets/js13kgames.png" alt="JS13K Games logo" height="24" />{' '}
<div class="footer__js13k-days-left">
{this.state.daysLeft} days to go
</div>
<div
class="footer__js13k-code-size"
style={{
color: codeSizeInKb > 10 ? 'crimson' : 'limegreen'
}}
>
{codeSizeInKb} KB/ 13KB
</div>
<span
class="caret"
style={`transition:0.3s ease; transform-origin: center 2px; ${
this.props.isOpen ? 'transform:rotate(180deg);' : ''
}`}
/>
{codeSizeInKb} KB/ 13KB
</div>
);
}
}
<span
className="caret"
style={{
transition: '0.3s ease',
transformOrigin: 'center 2px',
transform: props.isOpen ? 'rotate(180deg)' : ''
}}
/>
</div>
);
};
export default class Footer extends Component {
constructor(props) {
super(props);
this.state = {
isKeyboardShortcutsModalOpen: false,
isJs13kDropdownOpen: false
};
}
layoutBtnClickhandler(layoutId) {
this.props.layoutBtnClickHandler(layoutId);
export const Footer = props => {
const [isKeyboardShortcutsModalOpen, setIsKeyboardShortcutsModalOpen] =
useState(false);
const [isJs13kDropdownOpen, setIsJs13kDropdownOpen] = useState(false);
function layoutBtnClickhandler(layoutId) {
props.layoutBtnClickHandler(layoutId);
}
js13kClickHandler() {
function js13kClickHandler() {
// console.log(999);
this.setState({
isJs13kDropdownOpen: !this.state.isJs13kDropdownOpen
});
setIsJs13kDropdownOpen(!isJs13kDropdownOpen);
}
render() {
return (
<I18n>
{({ i18n }) => (
<div id="footer" class="footer">
<div>
<a href="/" target="_blank" rel="noopener noreferrer">
<div class="logo" />
</a>
&copy;
<span class="web-maker-with-tag">Web Maker</span> &nbsp;&nbsp;
<Button
onClick={this.props.helpBtnClickHandler}
data-event-category="ui"
data-event-action="helpButtonClick"
class="footer__link hint--rounded hint--top-right"
aria-label={i18n._(t`Help`)}
return (
<I18n>
{({ i18n }) => (
<div id="footer" class="footer">
<div>
<a href="/" target="_blank" rel="noopener noreferrer">
<div class="logo" />
</a>
&copy;
<span class="web-maker-with-tag">Web Maker</span> &nbsp;&nbsp;
<Button
onClick={props.helpBtnClickHandler}
data-event-category="ui"
data-event-action="helpButtonClick"
class="footer__link hint--rounded hint--top-right"
aria-label={i18n._(t`Help`)}
>
<svg
style="width:20px; height:20px; vertical-align:text-bottom"
viewBox="0 0 24 24"
>
<svg
style="width:20px; height:20px; vertical-align:text-bottom"
viewBox="0 0 24 24"
>
<path d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" />
</svg>
<path d="M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z" />
</svg>
</Button>
<Button
onClick={props.keyboardShortcutsBtnClickHandler}
data-event-category="ui"
data-event-action="keyboardShortcutButtonClick"
class="footer__link hint--rounded hint--top-right hide-on-mobile"
aria-label={i18n._(t`Keyboard shortcuts`)}
>
<svg
style={{
width: '20px',
height: '20px',
verticalAlign: 'text-bottom'
}}
>
<use xlinkHref="#keyboard-icon" />
</svg>
</Button>
<a
class="footer__link hint--rounded hint--top-right"
aria-label={i18n._(t`Tweet about 'Web Maker'`)}
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,frontend,playground,offline"
target="_blank"
rel="noopener noreferrer"
>
<svg
style={{
width: '20px',
height: '20px',
verticalAlign: 'text-bottom'
}}
>
<use xlinkHref="#twitter-icon" />
</svg>
</a>
{props.user?.isPro ? (
<Button
onClick={props.proBtnClickHandler}
data-event-category="ui"
data-event-action="manageProFooterBtnClick"
class="footer__link ml-1 hint--rounded hint--top-right hide-on-mobile support-link"
aria-label={i18n._(t`Manage your PRO subscription`)}
>
<HStack gap={1}>
<Trans>Manage</Trans>
<ProBadge />
</HStack>
</Button>
) : (
<Button
onClick={this.props.keyboardShortcutsBtnClickHandler}
onClick={props.proBtnClickHandler}
data-event-category="ui"
data-event-action="keyboardShortcutButtonClick"
class="footer__link hint--rounded hint--top-right hide-on-mobile"
aria-label={i18n._(t`Keyboard shortcuts`)}
>
<svg
style={{
width: '20px',
height: '20px',
verticalAlign: 'text-bottom'
}}
>
<use xlinkHref="#keyboard-icon" />
</svg>
</Button>
<a
class="footer__link hint--rounded hint--top-right"
aria-label={i18n._(t`Tweet about 'Web Maker'`)}
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,frontend,playground,offline"
target="_blank"
rel="noopener noreferrer"
>
<svg
style={{
width: '20px',
height: '20px',
verticalAlign: 'text-bottom'
}}
>
<use xlinkHref="#twitter-icon" />
</svg>
</a>
<Button
onClick={this.props.supportDeveloperBtnClickHandler}
data-event-category="ui"
data-event-action="supportDeveloperFooterBtnClick"
data-event-action="proFooterBtnClick"
class="footer__link ml-1 hint--rounded hint--top-right hide-on-mobile support-link"
aria-label={i18n._(
t`Support the developer by pledging some amount`
t`Upgrade to PRO and get some advanced superpowers!`
)}
>
<Trans>Donate</Trans>
<HStack gap={1}>
<Trans>Get</Trans>
<ProBadge />
</HStack>
</Button>
</div>
{this.props.prefs.isJs13kModeOn ? (
<div class="flex flex-v-center">
<JS13K
isOpen={this.state.isJs13kDropdownOpen}
codeSize={this.props.codeSize}
onClick={this.js13kClickHandler.bind(this)}
onBlur={() =>
setTimeout(
() => this.setState({ isJs13kDropdownOpen: false }),
300
)
}
/>
{this.state.isJs13kDropdownOpen && (
<div className="js13k__dropdown">
<button
class="btn"
style={{
width: '200px',
display: 'block',
marginBottom: '16px'
}}
onClick={this.props.onJs13KDownloadBtnClick}
>
<Trans>Download game as zip</Trans>
</button>
<a
class="btn"
rel="noopener"
style={{
width: '200px',
display: 'block',
marginBottom: '16px'
}}
href="https://pasteboard.co/"
target="_blank"
>
<Trans>Upload Image</Trans>
</a>
<button
class="btn"
style={{ width: '200px', display: 'block' }}
onClick={this.props.onJs13KHelpBtnClick}
>
<Trans>Help</Trans>
</button>
</div>
)}
</div>
) : null}
<div class="footer__right">
<button
onClick={this.props.saveHtmlBtnClickHandler}
id="saveHtmlBtn"
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Save as HTML file`)}
>
<svg viewBox="0 0 24 24">
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
</svg>
</button>
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
<symbol id="codepen-logo" viewBox="0 0 120 120">
<path
class="outer-ring"
d="M60.048 0C26.884 0 0 26.9 0 60.048s26.884 60 60 60.047c33.163 0 60.047-26.883 60.047-60.047 S93.211 0 60 0z M60.048 110.233c-27.673 0-50.186-22.514-50.186-50.186S32.375 9.9 60 9.9 c27.672 0 50.2 22.5 50.2 50.186S87.72 110.2 60 110.233z"
/>
<path
class="inner-box"
d="M97.147 48.319c-0.007-0.047-0.019-0.092-0.026-0.139c-0.016-0.09-0.032-0.18-0.056-0.268 c-0.014-0.053-0.033-0.104-0.05-0.154c-0.025-0.078-0.051-0.156-0.082-0.232c-0.021-0.053-0.047-0.105-0.071-0.156 c-0.033-0.072-0.068-0.143-0.108-0.211c-0.029-0.051-0.061-0.1-0.091-0.148c-0.043-0.066-0.087-0.131-0.135-0.193 c-0.035-0.047-0.072-0.094-0.109-0.139c-0.051-0.059-0.104-0.117-0.159-0.172c-0.042-0.043-0.083-0.086-0.127-0.125 c-0.059-0.053-0.119-0.104-0.181-0.152c-0.048-0.037-0.095-0.074-0.145-0.109c-0.019-0.012-0.035-0.027-0.053-0.039L61.817 23.5 c-1.072-0.715-2.468-0.715-3.54 0L24.34 46.081c-0.018 0.012-0.034 0.027-0.053 0.039c-0.05 0.035-0.097 0.072-0.144 0.1 c-0.062 0.049-0.123 0.1-0.181 0.152c-0.045 0.039-0.086 0.082-0.128 0.125c-0.056 0.055-0.108 0.113-0.158 0.2 c-0.038 0.045-0.075 0.092-0.11 0.139c-0.047 0.062-0.092 0.127-0.134 0.193c-0.032 0.049-0.062 0.098-0.092 0.1 c-0.039 0.068-0.074 0.139-0.108 0.211c-0.024 0.051-0.05 0.104-0.071 0.156c-0.031 0.076-0.057 0.154-0.082 0.2 c-0.017 0.051-0.035 0.102-0.05 0.154c-0.023 0.088-0.039 0.178-0.056 0.268c-0.008 0.047-0.02 0.092-0.025 0.1 c-0.019 0.137-0.029 0.275-0.029 0.416V71.36c0 0.1 0 0.3 0 0.418c0.006 0 0 0.1 0 0.1 c0.017 0.1 0 0.2 0.1 0.268c0.015 0.1 0 0.1 0.1 0.154c0.025 0.1 0.1 0.2 0.1 0.2 c0.021 0.1 0 0.1 0.1 0.154c0.034 0.1 0.1 0.1 0.1 0.213c0.029 0 0.1 0.1 0.1 0.1 c0.042 0.1 0.1 0.1 0.1 0.193c0.035 0 0.1 0.1 0.1 0.139c0.05 0.1 0.1 0.1 0.2 0.2 c0.042 0 0.1 0.1 0.1 0.125c0.058 0.1 0.1 0.1 0.2 0.152c0.047 0 0.1 0.1 0.1 0.1 c0.019 0 0 0 0.1 0.039L58.277 96.64c0.536 0.4 1.2 0.5 1.8 0.537c0.616 0 1.233-0.18 1.77-0.537 l33.938-22.625c0.018-0.012 0.034-0.027 0.053-0.039c0.05-0.035 0.097-0.072 0.145-0.109c0.062-0.049 0.122-0.1 0.181-0.152 c0.044-0.039 0.085-0.082 0.127-0.125c0.056-0.055 0.108-0.113 0.159-0.172c0.037-0.045 0.074-0.09 0.109-0.139 c0.048-0.062 0.092-0.127 0.135-0.193c0.03-0.049 0.062-0.098 0.091-0.146c0.04-0.07 0.075-0.141 0.108-0.213 c0.024-0.051 0.05-0.102 0.071-0.154c0.031-0.078 0.057-0.156 0.082-0.234c0.017-0.051 0.036-0.102 0.05-0.154 c0.023-0.088 0.04-0.178 0.056-0.268c0.008-0.045 0.02-0.092 0.026-0.137c0.018-0.139 0.028-0.277 0.028-0.418V48.735 C97.176 48.6 97.2 48.5 97.1 48.319z M63.238 32.073l25.001 16.666L77.072 56.21l-13.834-9.254V32.073z M56.856 32.1 v14.883L43.023 56.21l-11.168-7.471L56.856 32.073z M29.301 54.708l7.983 5.34l-7.983 5.34V54.708z M56.856 88.022L31.855 71.4 l11.168-7.469l13.833 9.252V88.022z M60.048 67.597l-11.286-7.549l11.286-7.549l11.285 7.549L60.048 67.597z M63.238 88.022V73.14 l13.834-9.252l11.167 7.469L63.238 88.022z M90.794 65.388l-7.982-5.34l7.982-5.34V65.388z"
/>
</symbol>
</svg>
<button
onClick={this.props.codepenBtnClickHandler}
id="codepenBtn"
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Edit on CodePen`)}
>
<svg>
<use xlinkHref="#codepen-logo" />
</svg>
</button>
<button
id="screenshotBtn"
class="mode-btn hint--rounded hint--top-left show-when-extension"
onClick={this.props.screenshotBtnClickHandler}
aria-label={i18n._(t`Take screenshot of preview`)}
>
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
</svg>
</button>
<div class="footer__separator hide-on-mobile" />
<button
onClick={this.layoutBtnClickhandler.bind(this, 1)}
id="layoutBtn1"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with preview on right`)}
>
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
<use xlinkHref="#mode-icon" />
</svg>
</button>
<button
onClick={this.layoutBtnClickhandler.bind(this, 2)}
id="layoutBtn2"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with preview on bottom`)}
>
<svg viewBox="0 0 100 100">
<use xlinkHref="#mode-icon" />
</svg>
</button>
<button
onClick={this.layoutBtnClickhandler.bind(this, 3)}
id="layoutBtn3"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with preview on left`)}
>
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
<use xlinkHref="#mode-icon" />
</svg>
</button>
<button
onClick={this.layoutBtnClickhandler.bind(this, 5)}
id="layoutBtn5"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with all vertical panes`)}
>
<svg viewBox="0 0 100 100">
<use xlinkHref="#vertical-mode-icon" />
</svg>
</button>
<button
onClick={this.layoutBtnClickhandler.bind(this, 4)}
id="layoutBtn4"
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
aria-label={i18n._(t`Toggle full screen preview`)}
>
<svg viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" />
</svg>
</button>
<button
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
aria-label={i18n._(t`Detach preview`)}
onClick={this.props.detachedPreviewBtnHandler}
>
<svg viewBox="0 0 24 24">
<path d="M22,17V7H6V17H22M22,5A2,2 0 0,1 24,7V17C24,18.11 23.1,19 22,19H16V21H18V23H10V21H12V19H6C4.89,19 4,18.11 4,17V7A2,2 0 0,1 6,5H22M2,3V15H0V3A2,2 0 0,1 2,1H20V3H2Z" />
</svg>
</button>
<div class="footer__separator" />
<button
onClick={this.props.notificationsBtnClickHandler}
id="notificationsBtn"
class={`notifications-btn mode-btn hint--top-left hint--rounded ${
this.props.hasUnseenChangelog ? 'has-new' : ''
}`}
aria-label={i18n._(t`See changelog`)}
>
<svg viewBox="0 0 24 24">
<path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V16L21,19H3L6,16V10C6,7.03 8.16,4.56 11,4.08V3A1,1 0 0,1 12,2Z" />
</svg>
<span class="notifications-btn__dot" />
</button>
<Button
onClick={this.props.settingsBtnClickHandler}
data-event-category="ui"
data-event-action="settingsBtnClick"
class="mode-btn hint--top-left hint--rounded"
aria-label={i18n._(t`Settings`)}
>
<svg>
<use xlinkHref="#settings-icon" />
</svg>
</Button>
</div>
)}
</div>
)}
</I18n>
);
}
}
{props.prefs.isJs13kModeOn ? (
<div class="flex flex-v-center">
<JS13K
isOpen={isJs13kDropdownOpen}
codeSize={props.codeSize}
onClick={js13kClickHandler}
onBlur={() =>
setTimeout(() => setIsJs13kDropdownOpen(false), 300)
}
/>
{isJs13kDropdownOpen && (
<div className="js13k__dropdown">
<button
class="btn"
style={{
width: '200px',
display: 'block',
marginBottom: '16px'
}}
onClick={props.onJs13KDownloadBtnClick}
>
<Trans>Download game as zip</Trans>
</button>
<a
class="btn"
rel="noopener"
style={{
width: '200px',
display: 'block',
marginBottom: '16px'
}}
href="https://pasteboard.co/"
target="_blank"
>
<Trans>Upload Image</Trans>
</a>
<button
class="btn"
style={{ width: '200px', display: 'block' }}
onClick={props.onJs13KHelpBtnClick}
>
<Trans>Help</Trans>
</button>
</div>
)}
</div>
) : null}
<div class="footer__right">
<button
onClick={props.saveHtmlBtnClickHandler}
id="saveHtmlBtn"
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Save as HTML file`)}
>
<svg viewBox="0 0 24 24">
<path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
</svg>
</button>
<svg style="display: none;" xmlns="http://www.w3.org/2000/svg">
<symbol id="codepen-logo" viewBox="0 0 120 120">
<path
class="outer-ring"
d="M60.048 0C26.884 0 0 26.9 0 60.048s26.884 60 60 60.047c33.163 0 60.047-26.883 60.047-60.047 S93.211 0 60 0z M60.048 110.233c-27.673 0-50.186-22.514-50.186-50.186S32.375 9.9 60 9.9 c27.672 0 50.2 22.5 50.2 50.186S87.72 110.2 60 110.233z"
/>
<path
class="inner-box"
d="M97.147 48.319c-0.007-0.047-0.019-0.092-0.026-0.139c-0.016-0.09-0.032-0.18-0.056-0.268 c-0.014-0.053-0.033-0.104-0.05-0.154c-0.025-0.078-0.051-0.156-0.082-0.232c-0.021-0.053-0.047-0.105-0.071-0.156 c-0.033-0.072-0.068-0.143-0.108-0.211c-0.029-0.051-0.061-0.1-0.091-0.148c-0.043-0.066-0.087-0.131-0.135-0.193 c-0.035-0.047-0.072-0.094-0.109-0.139c-0.051-0.059-0.104-0.117-0.159-0.172c-0.042-0.043-0.083-0.086-0.127-0.125 c-0.059-0.053-0.119-0.104-0.181-0.152c-0.048-0.037-0.095-0.074-0.145-0.109c-0.019-0.012-0.035-0.027-0.053-0.039L61.817 23.5 c-1.072-0.715-2.468-0.715-3.54 0L24.34 46.081c-0.018 0.012-0.034 0.027-0.053 0.039c-0.05 0.035-0.097 0.072-0.144 0.1 c-0.062 0.049-0.123 0.1-0.181 0.152c-0.045 0.039-0.086 0.082-0.128 0.125c-0.056 0.055-0.108 0.113-0.158 0.2 c-0.038 0.045-0.075 0.092-0.11 0.139c-0.047 0.062-0.092 0.127-0.134 0.193c-0.032 0.049-0.062 0.098-0.092 0.1 c-0.039 0.068-0.074 0.139-0.108 0.211c-0.024 0.051-0.05 0.104-0.071 0.156c-0.031 0.076-0.057 0.154-0.082 0.2 c-0.017 0.051-0.035 0.102-0.05 0.154c-0.023 0.088-0.039 0.178-0.056 0.268c-0.008 0.047-0.02 0.092-0.025 0.1 c-0.019 0.137-0.029 0.275-0.029 0.416V71.36c0 0.1 0 0.3 0 0.418c0.006 0 0 0.1 0 0.1 c0.017 0.1 0 0.2 0.1 0.268c0.015 0.1 0 0.1 0.1 0.154c0.025 0.1 0.1 0.2 0.1 0.2 c0.021 0.1 0 0.1 0.1 0.154c0.034 0.1 0.1 0.1 0.1 0.213c0.029 0 0.1 0.1 0.1 0.1 c0.042 0.1 0.1 0.1 0.1 0.193c0.035 0 0.1 0.1 0.1 0.139c0.05 0.1 0.1 0.1 0.2 0.2 c0.042 0 0.1 0.1 0.1 0.125c0.058 0.1 0.1 0.1 0.2 0.152c0.047 0 0.1 0.1 0.1 0.1 c0.019 0 0 0 0.1 0.039L58.277 96.64c0.536 0.4 1.2 0.5 1.8 0.537c0.616 0 1.233-0.18 1.77-0.537 l33.938-22.625c0.018-0.012 0.034-0.027 0.053-0.039c0.05-0.035 0.097-0.072 0.145-0.109c0.062-0.049 0.122-0.1 0.181-0.152 c0.044-0.039 0.085-0.082 0.127-0.125c0.056-0.055 0.108-0.113 0.159-0.172c0.037-0.045 0.074-0.09 0.109-0.139 c0.048-0.062 0.092-0.127 0.135-0.193c0.03-0.049 0.062-0.098 0.091-0.146c0.04-0.07 0.075-0.141 0.108-0.213 c0.024-0.051 0.05-0.102 0.071-0.154c0.031-0.078 0.057-0.156 0.082-0.234c0.017-0.051 0.036-0.102 0.05-0.154 c0.023-0.088 0.04-0.178 0.056-0.268c0.008-0.045 0.02-0.092 0.026-0.137c0.018-0.139 0.028-0.277 0.028-0.418V48.735 C97.176 48.6 97.2 48.5 97.1 48.319z M63.238 32.073l25.001 16.666L77.072 56.21l-13.834-9.254V32.073z M56.856 32.1 v14.883L43.023 56.21l-11.168-7.471L56.856 32.073z M29.301 54.708l7.983 5.34l-7.983 5.34V54.708z M56.856 88.022L31.855 71.4 l11.168-7.469l13.833 9.252V88.022z M60.048 67.597l-11.286-7.549l11.286-7.549l11.285 7.549L60.048 67.597z M63.238 88.022V73.14 l13.834-9.252l11.167 7.469L63.238 88.022z M90.794 65.388l-7.982-5.34l7.982-5.34V65.388z"
/>
</symbol>
</svg>
<button
onClick={props.codepenBtnClickHandler}
id="codepenBtn"
class="mode-btn hint--rounded hint--top-left hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Edit on CodePen`)}
>
<svg>
<use xlinkHref="#codepen-logo" />
</svg>
</button>
<button
id="screenshotBtn"
class="mode-btn hint--rounded hint--top-left show-when-extension"
onClick={props.screenshotBtnClickHandler}
aria-label={i18n._(t`Take screenshot of preview`)}
>
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
<path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
</svg>
</button>
<div class="footer__separator hide-on-mobile" />
<button
onClick={() => layoutBtnClickhandler(1)}
id="layoutBtn1"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with preview on right`)}
>
<svg viewBox="0 0 100 100" style="transform:rotate(-90deg)">
<use xlinkHref="#mode-icon" />
</svg>
</button>
<button
onClick={() => layoutBtnClickhandler(2)}
id="layoutBtn2"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with preview on bottom`)}
>
<svg viewBox="0 0 100 100">
<use xlinkHref="#mode-icon" />
</svg>
</button>
<button
onClick={() => layoutBtnClickhandler(3)}
id="layoutBtn3"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with preview on left`)}
>
<svg viewBox="0 0 100 100" style="transform:rotate(90deg)">
<use xlinkHref="#mode-icon" />
</svg>
</button>
<button
onClick={() => layoutBtnClickhandler(5)}
id="layoutBtn5"
class="mode-btn hide-on-mobile hide-in-file-mode"
aria-label={i18n._(t`Switch to layout with all vertical panes`)}
>
<svg viewBox="0 0 100 100">
<use xlinkHref="#vertical-mode-icon" />
</svg>
</button>
<button
onClick={() => layoutBtnClickhandler(4)}
id="layoutBtn4"
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
aria-label={i18n._(t`Toggle full screen preview`)}
>
<svg viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" />
</svg>
</button>
<button
class="mode-btn hint--top-left hint--rounded hide-on-mobile"
aria-label={i18n._(t`Detach preview`)}
onClick={props.detachedPreviewBtnHandler}
>
<svg viewBox="0 0 24 24">
<path d="M22,17V7H6V17H22M22,5A2,2 0 0,1 24,7V17C24,18.11 23.1,19 22,19H16V21H18V23H10V21H12V19H6C4.89,19 4,18.11 4,17V7A2,2 0 0,1 6,5H22M2,3V15H0V3A2,2 0 0,1 2,1H20V3H2Z" />
</svg>
</button>
<div class="footer__separator" />
<button
onClick={props.notificationsBtnClickHandler}
id="notificationsBtn"
class={`notifications-btn mode-btn hint--top-left hint--rounded ${
props.hasUnseenChangelog ? 'has-new' : ''
}`}
aria-label={i18n._(t`See changelog`)}
>
<svg viewBox="0 0 24 24">
<path d="M14,20A2,2 0 0,1 12,22A2,2 0 0,1 10,20H14M12,2A1,1 0 0,1 13,3V4.08C15.84,4.56 18,7.03 18,10V16L21,19H3L6,16V10C6,7.03 8.16,4.56 11,4.08V3A1,1 0 0,1 12,2Z" />
</svg>
<span class="notifications-btn__dot" />
</button>
<Button
onClick={props.settingsBtnClickHandler}
data-event-category="ui"
data-event-action="settingsBtnClick"
class="mode-btn hint--top-left hint--rounded"
aria-label={i18n._(t`Settings`)}
>
<svg>
<use xlinkHref="#settings-icon" />
</svg>
</Button>
</div>
</div>
)}
</I18n>
);
};

View File

@@ -1,5 +1,5 @@
import { h } from 'preact';
import Modal from './Modal';
import { Stack, VStack } from './Stack';
import { Button } from './common';
import { Trans } from '@lingui/macro';
@@ -7,15 +7,21 @@ export function HelpModal(props) {
return (
<Modal show={props.show} closeHandler={props.closeHandler}>
<h1>
<div class="web-maker-with-tag">Web Maker</div>
<small style="font-size:14px;">{props.version}</small>
<Stack gap={1} align="center">
<div class="web-maker-with-tag">Web Maker</div>
<span className="badge">{props.version}</span>
</Stack>
</h1>
<div>
<p>
<Trans>
Made with <span style="margin-right: 8px;">💖</span>&{' '}
<span style="margin-right: 8px;"> 🙌</span> by{' '}
<span style="margin-right: 0.2rem;position:relative;top:-3px;">
{' '}
🙌
</span>{' '}
by{' '}
<a
href="https://twitter.com/chinchang457"
target="_blank"
@@ -43,11 +49,11 @@ export function HelpModal(props) {
</Trans>
.
</p>
<p class="show-when-extension">
<p>
<Trans>
Like this extension? Please{' '}
Like this app? Please{' '}
<a
href="https://chrome.google.com/webstore/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
href="https://chromewebstore.google.com/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
target="_blank"
rel="noopener noreferrer"
>
@@ -56,20 +62,16 @@ export function HelpModal(props) {
.
</Trans>
</p>
<p>
<Button
onClick={props.onSupportBtnClick}
data-event-action="supportDeveloperHelpBtnClick"
data-event-category="ui"
<p style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<a
href="http://twitter.com/share?url=https://webmaker.app/&text=Web Maker - A blazing fast %26 offline web playground! via @webmakerApp&related=webmakerApp&hashtags=web,frontend,playground,offline"
rel="noopener noreferrer"
class="btn btn-icon"
>
<svg>
<use xlinkHref="#gift-icon" />
</svg>
<Trans>Support the developer</Trans>
</Button>{' '}
<Trans>Share Web Maker</Trans>
</a>{' '}
<a
href="https://chrome.google.com/webstore/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
href="https://chromewebstore.google.com/detail/web-maker/lkfkkhfhhdkiemehlpkgjeojomhpccnh/reviews"
target="_blank"
rel="noopener noreferrer"
class="btn btn-icon"
@@ -80,7 +82,7 @@ export function HelpModal(props) {
<Trans>Review Web Maker</Trans>
</a>{' '}
<a
href="https://spectrum.chat/web-maker"
href="https://github.com/chinchang/web-maker/discussions"
target="_blank"
rel="noopener noreferrer"
class="btn btn-icon"
@@ -88,7 +90,7 @@ export function HelpModal(props) {
<svg>
<use xlinkHref="#chat-icon" />
</svg>
<Trans>Chat</Trans>
<Trans>Discuss</Trans>
</a>{' '}
<a
href="https://github.com/chinchang/web-maker/issues"
@@ -190,29 +192,31 @@ export function HelpModal(props) {
</details>
</p>
<p>
<VStack gap={1} align="stretch" fullWidth={true}>
<h3>
<Trans>License</Trans>
</h3>
<Trans>
"Web Maker" is{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/chinchang/web-maker"
>
open-source
</a>{' '}
under the{' '}
<a
href="https://opensource.org/licenses/MIT"
target="_blank"
rel="noopener noreferrer"
>
MIT License
</a>
</Trans>
</p>
<p>
<Trans>
"Web Maker" is{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://github.com/chinchang/web-maker"
>
open-source
</a>{' '}
under the{' '}
<a
href="https://opensource.org/licenses/MIT"
target="_blank"
rel="noopener noreferrer"
>
MIT License
</a>
</Trans>
</p>
</VStack>
</div>
</Modal>
);

View File

@@ -1,5 +1,3 @@
import { h } from 'preact';
export function Icons() {
return (
<svg
@@ -109,6 +107,30 @@ export function Icons() {
<symbol id="search" viewBox="0 0 24 24">
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" />
</symbol>
<symbol id="copy" viewBox="0 0 24 24">
<path d="M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1Z" />
</symbol>
<symbol id="trash" viewBox="0 0 24 24">
<path d="M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3H9M7 6h10v13H7V6m2 2v9h2V8H9m4 0v9h2V8h-2Z" />
</symbol>
<symbol id="view-grid" viewBox="0 0 24 24">
<path d="M3 11h8V3H3m0 18h8v-8H3m10 8h8v-8h-8m0-10v8h8V3" />
</symbol>
<symbol id="view-list" viewBox="0 0 24 24">
<path d="M9,5V9H21V5M9,19H21V15H9M9,14H21V10H9M4,9H8V5H4M4,19H8V15H4M4,14H8V10H4V14Z" />
</symbol>
<symbol id="eye" viewBox="0 0 24 24">
<path d="M12 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0 8a5 5 0 0 1-5-5 5 5 0 0 1 5-5 5 5 0 0 1 5 5 5 5 0 0 1-5 5m0-12.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5Z" />
</symbol>
<symbol id="eye-striked" viewBox="0 0 24 24">
<path d="M11.83 9 15 12.16V12a3 3 0 0 0-3-3h-.17m-4.3.8 1.55 1.55c-.05.21-.08.42-.08.65a3 3 0 0 0 3 3c.22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53a5 5 0 0 1-5-5c0-.79.2-1.53.53-2.2M2 4.27l2.28 2.28.45.45C3.08 8.3 1.78 10 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.43.42L19.73 22 21 20.73 3.27 3M12 7a5 5 0 0 1 5 5c0 .64-.13 1.26-.36 1.82l2.93 2.93c1.5-1.25 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-4 .7l2.17 2.15C10.74 7.13 11.35 7 12 7Z" />
</symbol>
<symbol id="check-circle" viewBox="0 0 24 24">
<path d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" />
</symbol>
<symbol id="fork" viewBox="0 0 24 24">
<path d="M13 14c-3.36 0-4.46 1.35-4.82 2.24C9.25 16.7 10 17.76 10 19a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1.31.83-2.42 2-2.83V7.83A2.99 2.99 0 0 1 4 5a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.31-.83 2.42-2 2.83v5.29c.88-.65 2.16-1.12 4-1.12 2.67 0 3.56-1.34 3.85-2.23A3.006 3.006 0 0 1 14 7a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.34-.88 2.5-2.09 2.86C17.65 11.29 16.68 14 13 14m-6 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1M7 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1m10 2a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1Z" />
</symbol>
<symbol id="loader-icon" viewBox="0 0 44 44">
{/* By Sam Herbert (@sherb), for everyone. More http://goo.gl/7AJzbL */}
<g fill="none" fillRule="evenodd" strokeWidth={10}>
@@ -162,3 +184,17 @@ export function Icons() {
</svg>
);
}
export const Icon = ({ name, color = 'currentColor', size, ...rest }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
style={{ fill: color }}
{...rest}
>
<use xlinkHref={`#${name}`} />
</svg>
);
};

View File

@@ -1,14 +1,18 @@
import { h } from 'preact';
import { getHumanDate } from '../utils';
import Modal from './Modal';
import { HStack, Stack } from './Stack';
import { Icon } from './Icons';
export function ItemTile({
item,
onClick,
onForkBtnClick,
onRemoveBtnClick,
onToggleVisibilityBtnClick,
focusable,
inline
inline,
hasOptions = true
}) {
return (
<div
@@ -27,6 +31,13 @@ export function ItemTile({
aria-label="Creates a duplicate of this creation (Ctrl/⌘ + F)"
onClick={onForkBtnClick}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M13 14c-3.36 0-4.46 1.35-4.82 2.24C9.25 16.7 10 17.76 10 19a3 3 0 0 1-3 3 3 3 0 0 1-3-3c0-1.31.83-2.42 2-2.83V7.83A2.99 2.99 0 0 1 4 5a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.31-.83 2.42-2 2.83v5.29c.88-.65 2.16-1.12 4-1.12 2.67 0 3.56-1.34 3.85-2.23A3.006 3.006 0 0 1 14 7a3 3 0 0 1 3-3 3 3 0 0 1 3 3c0 1.34-.88 2.5-2.09 2.86C17.65 11.29 16.68 14 13 14m-6 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1M7 4a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1m10 2a1 1 0 0 0-1 1 1 1 0 0 0 1 1 1 1 0 0 0 1-1 1 1 0 0 0-1-1Z" />
</svg>
Fork<span class="show-when-selected">(Ctrl/ + F)</span>
</button>
) : null}
@@ -36,13 +47,19 @@ export function ItemTile({
aria-label="Remove"
onClick={onRemoveBtnClick}
>
X
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M9 3v1H4v2h1v13a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1V4h-5V3H9M7 6h10v13H7V6m2 2v9h2V8H9m4 0v9h2V8h-2Z" />
</svg>
</button>
) : null}
</div>
<div className="flex flex-v-center">
{item.img ? (
<div>
<div class="d-f">
<img
class="saved-item-tile__img"
height="40"
@@ -66,12 +83,32 @@ export function ItemTile({
</div>
) : null}
</div>
{item.updatedOn ? (
{hasOptions && (
<div class="saved-item-tile__meta">
Last updated:{' '}
<time dateTime={item.updatedOn}>{getHumanDate(item.updatedOn)}</time>
<HStack justify="space-between">
<div>
{item.updatedOn ? (
<>
Last updated:{' '}
<time dateTime={item.updatedOn}>
{getHumanDate(item.updatedOn)}
</time>
</>
) : null}
</div>
<div>
<Stack gap={1} align="center">
<Icon
size="16"
color="currentColor"
name={item.isPublic ? 'eye' : 'eye-striked'}
/>
{item.isPublic ? 'Public' : ''}
</Stack>
</div>
</HStack>
</div>
) : null}
)}
</div>
);
}

52
src/components/Loader.jsx Normal file
View File

@@ -0,0 +1,52 @@
export function Loader({ height, noMargin, leftMargin }) {
return (
<svg
viewBox="0 0 166 166"
height={height || '1.6em'}
style={{
margin: noMargin
? null
: leftMargin
? ` 0 0 0 ${leftMargin}`
: '0 0.8rem'
}}
class="new-loader"
>
<g fill="none" fillRule="evenodd">
<path
d="M83 166c-45.84 0-83-37.16-83-83S37.16 0 83 0s83 37.16 83 83-37.16 83-83 83zm0-29c29.823 0 54-24.177 54-54s-24.177-54-54-54-54 24.177-54 54 24.177 54 54 54z"
fill="currentColor"
style={{ opacity: 0.2 }}
/>
<path
d="M137.008 83H137c0-29.823-24.177-54-54-54S29 53.177 29 83h-.008c.005.166.008.333.008.5C29 91.508 22.508 98 14.5 98S0 91.508 0 83.5c0-.167.003-.334.008-.5H0C0 37.16 37.16 0 83 0s83 37.16 83 83h-.008c.005.166.008.333.008.5 0 8.008-6.492 14.5-14.5 14.5S137 91.508 137 83.5c0-.167.003-.334.008-.5z"
fill="currentColor"
/>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
dur="1s"
from="0 83 83"
to="360 83 83"
repeatCount="indefinite"
/>
</g>
</svg>
);
}
export function LoaderWithText({ children }) {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
margin: '2rem 1rem'
}}
>
<Loader /> {children}
</div>
);
}

View File

@@ -54,21 +54,8 @@ export default class Login extends Component {
Login with Google
</button>
</p>
<p class="mb-2">
<button
type="button"
onClick={this.login.bind(this)}
class="social-login-btn social-login-btn--facebook btn btn-icon btn--big full-width hint--right hint--always"
data-auth-provider="facebook"
data-hint="You logged in with Facebook last time"
>
<svg>
<use xlinkHref="#fb-icon" />
</svg>
Login with Facebook (deprecated)
</button>
</p>
<p>Join a community of 50,000+ Developers</p>
<p>Join a community of 70,000+ Developers</p>
</div>
</div>
);

View File

@@ -2,16 +2,32 @@ import { h } from 'preact';
import { Button } from './common';
import { Trans, NumberFormat, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { ProBadge } from './ProBadge';
import { HStack, Stack } from './Stack';
import { Icon } from './Icons';
const DEFAULT_PROFILE_IMG =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z'/%3E%3C/svg%3E";
export function MainHeader(props) {
export function MainHeader({
user,
currentItem,
titleInputBlurHandler,
runBtnClickHandler,
assetsBtnHandler,
isFileMode,
onItemFork,
...props
}) {
const isAutoPreviewOn =
window.forcedSettings.autoPreview !== undefined
? window.forcedSettings
: props.isAutoPreviewOn;
const isNotMine =
currentItem.createdBy && user?.uid !== currentItem.createdBy;
// console.log(33, currentItem, user?.uid);
return (
<I18n>
{({ i18n }) => (
@@ -21,15 +37,15 @@ export function MainHeader(props) {
id="titleInput"
title="Click to edit"
class="item-title-input"
value={props.title}
onBlur={props.titleInputBlurHandler}
value={currentItem.title}
onBlur={titleInputBlurHandler}
/>
<div class="main-header__btn-wrap flex flex-v-center">
{!isAutoPreviewOn && (
<button
class="btn btn btn--dark flex flex-v-center hint--rounded hint--bottom-left"
aria-label={i18n._(t`Run preview (Ctrl/⌘ + Shift + 5)`)}
onClick={props.runBtnClickHandler}
onClick={runBtnClickHandler}
>
<svg>
<use xlinkHref="#play-icon" />
@@ -37,8 +53,17 @@ export function MainHeader(props) {
<Trans>Run</Trans>
</button>
)}
{!props.isFileMode && (
<Button
onClick={assetsBtnHandler}
data-event-category="ui"
data-event-action="addLibraryButtonClick"
data-testid="addLibraryButton"
class="btn btn--dark hint--rounded hint--bottom-left"
aria-label={i18n._(t`Upload/Use assets`)}
>
<Trans>Assets</Trans>
</Button>
{!isFileMode && (
<Button
onClick={props.addLibraryBtnHandler}
data-event-category="ui"
@@ -60,6 +85,38 @@ export function MainHeader(props) {
</Button>
)}
{!isNotMine && (
<button
class="btn btn--dark hint--bottom-left"
aria-label={i18n._(t`Share this creation publicly`)}
data-testid="newButton"
onClick={props.shareBtnHandler}
>
<svg
viewBox="0 0 24 24"
style={{
fill: currentItem.isPublic ? 'limegreen' : 'currentColor'
}}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 16.08C17.24 16.08 16.56 16.38 16.04 16.85L8.91 12.7C8.96 12.47 9 12.24 9 12S8.96 11.53 8.91 11.3L15.96 7.19C16.5 7.69 17.21 8 18 8C19.66 8 21 6.66 21 5S19.66 2 18 2 15 3.34 15 5C15 5.24 15.04 5.47 15.09 5.7L8.04 9.81C7.5 9.31 6.79 9 6 9C4.34 9 3 10.34 3 12S4.34 15 6 15C6.79 15 7.5 14.69 8.04 14.19L15.16 18.34C15.11 18.55 15.08 18.77 15.08 19C15.08 20.61 16.39 21.91 18 21.91S20.92 20.61 20.92 19C20.92 17.39 19.61 16.08 18 16.08M18 4C18.55 4 19 4.45 19 5S18.55 6 18 6 17 5.55 17 5 17.45 4 18 4M6 13C5.45 13 5 12.55 5 12S5.45 11 6 11 7 11.45 7 12 6.55 13 6 13M18 20C17.45 20 17 19.55 17 19S17.45 18 18 18 19 18.45 19 19 18.55 20 18 20Z" />
</svg>
{currentItem.isPublic ? null : <Trans>Share</Trans>}
</button>
)}
<button
class="btn btn--dark hint--bottom-left"
aria-label={i18n._(t`Fork this creation`)}
data-testid="headerForkButton"
onClick={onItemFork}
>
<Icon name="fork" />
<Trans>Fork</Trans>
</button>
<button
class="btn btn--dark hint--rounded hint--bottom-left"
aria-label={i18n._(t`Start a new creation`)}
@@ -71,22 +128,25 @@ export function MainHeader(props) {
</svg>
<Trans>New</Trans>
</button>
<button
id="saveBtn"
class={`btn btn--dark hint--rounded hint--bottom-left ${
props.isSaving ? 'is-loading' : ''
} ${props.unsavedEditCount ? 'is-marked' : 0}`}
aria-label={i18n._(t`Save current creation (Ctrl/⌘ + S)`)}
onClick={props.saveBtnHandler}
>
<svg viewBox="0 0 24 24">
<path d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
</svg>
<svg class="btn-loader" width="15" height="15" stroke="#fff">
<use xlinkHref="#loader-icon" />
</svg>
<Trans>Save</Trans>
</button>
{!isNotMine && (
<button
id="saveBtn"
class={`btn btn--dark hint--rounded hint--bottom-left ${
props.isSaving ? 'is-loading' : ''
} ${props.unsavedEditCount ? 'is-marked' : 0}`}
aria-label={i18n._(t`Save current creation (Ctrl/⌘ + S)`)}
onClick={props.saveBtnHandler}
>
<svg viewBox="0 0 24 24">
<path d="M15,9H5V5H15M12,19A3,3 0 0,1 9,16A3,3 0 0,1 12,13A3,3 0 0,1 15,16A3,3 0 0,1 12,19M17,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V7L17,3Z" />
</svg>
<svg class="btn-loader" width="15" height="15" stroke="#fff">
<use xlinkHref="#loader-icon" />
</svg>
<Trans>Save</Trans>
</button>
)}
<button
id="openItemsBtn"
class={`btn btn--dark hint--rounded hint--bottom-left ${
@@ -103,13 +163,13 @@ export function MainHeader(props) {
</svg>
<Trans>Open</Trans>
</button>
{!props.user ? (
{!user ? (
<Button
onClick={props.loginBtnHandler}
data-event-category="ui"
data-event-action="loginButtonClick"
data-testid="loginButton"
class="btn btn--dark hint--rounded hint--bottom-left"
class="btn btn--dark"
>
<Trans>Login/Signup</Trans>
</Button>
@@ -121,14 +181,15 @@ export function MainHeader(props) {
aria-label={i18n._(t`See profile or Logout`)}
class="btn--dark hint--rounded hint--bottom-left"
>
<img
id="headerAvatarImg"
width="20"
src={
props.user ? props.user.photoURL || DEFAULT_PROFILE_IMG : ''
}
class="main-header__avatar-img"
/>
<HStack gap={1}>
<img
id="headerAvatarImg"
width="20"
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
class="main-header__avatar-img"
/>
{user?.isPro ? <ProBadge /> : null}
</HStack>
</Button>
)}
</div>

View File

@@ -77,31 +77,47 @@ const Modal = ({
if (!show) return null;
return (
<Portal into={`body`}>
<div
role="dialog"
class={`${extraClasses || ''} modal is-modal-visible ${
small ? 'modal--small' : ''
}`}
ref={overlayRef}
onClick={onOverlayClick}
>
<div class="modal__content">
{hideCloseButton ? null : (
<button
type="button"
onClick={closeHandler}
aria-label="Close modal"
data-testid="closeModalButton"
title="Close"
class="js-modal__close-btn modal__close-btn"
>
Close
</button>
)}
{children}
<Portal into={`#portal`}>
<>
{/* <div class="modal-overlay" /> */}
<div
role="dialog"
class={`${extraClasses || ''} modal is-modal-visible ${
small ? 'modal--small' : ''
}
${noOverlay ? 'modal--no-overlay' : ''}
`}
ref={overlayRef}
onClick={onOverlayClick}
>
<div class="modal__content">
{hideCloseButton ? null : (
<button
type="button"
onClick={closeHandler}
aria-label="Close modal"
data-testid="closeModalButton"
title="Close"
class="js-modal__close-btn dialog__close-btn modal__close-btn"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
)}
{children}
</div>
</div>
</div>
</>
</Portal>
);
};

View File

@@ -1,84 +1,93 @@
import { h } from 'preact';
import Modal from './Modal.jsx';
import { Stack } from './Stack.jsx';
export function OnboardingModal(props) {
return (
<Modal show={props.show} closeHandler={props.closeHandler}>
<div class="tac">
<svg width="130px" height="50px" aria-hidden="true">
<Stack gap={3} justify="center" align="center">
<svg width="83" height="32" aria-hidden="true">
<use xlinkHref="#logo" />
</svg>
<h1 style="margin-top:20px">Welcome to Web Maker</h1>
</div>
<h1>Welcome to Web Maker</h1>
</Stack>
<div class="flex--desk" style="margin-top:40px;">
<div class="onboard-step show-when-app hide-on-mobile">
<div class="tac">
<div
class="flex--desk"
style="margin-top:40px; display:grid; grid-template-columns:1fr 1fr"
>
<div class="onboard-step onboard-step--full-width show-when-app hide-on-mobile">
<Stack gap={1} align="center">
<svg class="onboard-step__icon" viewBox="0 0 24 24">
<path d="M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" />
</svg>
</div>
<p>
Open Web Maker anytime by visiting <a>https://webmaker.app/app/</a>{' '}
- Even when you are offline! It just works! 😱{' '}
<strong>Drag the following bookmarklet</strong> on your bookmark bar
to create a quick access shortcut:
<a class="ml-1 bookmarklet" href="https://webmaker.app/app/">
<svg width="20" height="20" aria-hidden="true">
<p>
Open Web Maker anytime by visiting{' '}
<a>https://webmaker.app/create/</a> - Even when you are offline!
It just works! 😱 <strong>Drag the following bookmarklet</strong>{' '}
on your bookmark bar to create a quick access shortcut:
<a class="ml-1 bookmarklet" href="https://webmaker.app/create/">
<svg width="20" height="20" aria-hidden="true">
<use xlinkHref="#logo" />
</svg>
Web Maker
</a>
</p>
</Stack>
</div>
<div class="onboard-step onboard-step--full-width show-when-extension">
<Stack gap={1} align="center">
<svg class="onboard-step__icon" viewBox="0 0 24 24">
<path d="M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" />
</svg>
<p>
Open Web Maker anytime by clicking the
<svg class="relative" style="top:5px;" width="40" height="30">
<use xlinkHref="#logo" />
</svg>{' '}
button in top-right side of your browser.
</p>
</Stack>
</div>
<div class="onboard-step">
<Stack gap={1} align="center">
<svg class="onboard-step__icon" viewBox="0 0 24 24">
<use xlinkHref="#settings-icon" />
</svg>
<p>
Configure and customize settings by clicking the gear icon (
<svg
style="width:18px;height:18px;position:relative;top:3px;fill:#888"
viewBox="0 0 24 24"
>
<use xlinkHref="#settings-icon" />
</svg>
Web Maker
</a>
</p>
</div>
<div class="onboard-step show-when-extension">
<div class="tac">
<svg class="onboard-step__icon" viewBox="0 0 24 24">
<path d="M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" />
</svg>
</div>
<p>
Open Web Maker anytime by clicking the
<svg class="relative" style="top:5px;" width="40" height="30">
<use xlinkHref="#logo" />
</svg>{' '}
button in top-right side of your browser.
</p>
) in bottom right of the app.
</p>
</Stack>
</div>
<div class="onboard-step">
<div class="tac">
<svg class="onboard-step__icon" viewBox="0 0 24 24">
<use xlinkHref="#settings-icon" />
</svg>
</div>
<p>
Configure and customize settings by clicking the gear icon (
<svg
style="width:18px;height:18px;position:relative;top:3px;fill:#888"
viewBox="0 0 24 24"
>
<use xlinkHref="#settings-icon" />
</svg>
) in bottom right of the app.
</p>
</div>
<div class="onboard-step">
<div class="tac">
<Stack gap={1} align="center">
<svg class="onboard-step__icon" style="stroke-width:0.3px;">
<use xlinkHref="#twitter-icon" />
</svg>
</div>
<p>
Follow{' '}
<a
href="https://twitter.com/intent/follow?screen_name=webmakerApp"
targe="_blank"
rel="noopener noreferrer"
>
@webmakerApp
</a>{' '}
to know about the new upcoming features!
</p>
<p>
Follow{' '}
<a
href="https://twitter.com/intent/follow?screen_name=webmakerApp"
targe="_blank"
rel="noopener noreferrer"
>
@webmakerApp
</a>{' '}
to know about the new upcoming features!
</p>
</Stack>
</div>
</div>
@@ -95,11 +104,11 @@ export function OnboardingModal(props) {
.
</p>
<p class="tac">
<Stack justify="center">
<button class="btn btn--primary" onClick={props.closeHandler}>
Lets start!
</button>
</p>
</Stack>
</Modal>
);
}

31
src/components/Panel.jsx Normal file
View File

@@ -0,0 +1,31 @@
import { forwardRef } from 'preact/compat';
export const Panel = forwardRef(function Panel(
{
classes = '',
padding = '2rem',
fullWidth = true,
fullHeight = false,
glowing = false,
topFocus = false,
onlyBorder = false,
children
},
ref
) {
return (
<div
ref={ref}
style={{
padding: padding,
width: fullWidth ? '100%' : 'auto',
height: fullHeight ? '100%' : 'auto'
}}
className={`panel ${classes} ${glowing && 'panelGlowing'} ${
topFocus && 'panelTopFocus'
} ${onlyBorder && 'panelOnlyBorder'}`}
>
{children}
</div>
);
});

157
src/components/Pro.jsx Normal file
View File

@@ -0,0 +1,157 @@
import { useEffect, useState } from 'preact/hooks';
import { ProBadge } from './ProBadge';
import { HStack, Stack, VStack } from './Stack';
import Switch from './Switch';
import { alertsService } from '../notifications';
import { A, Button } from './common';
import { useCheckout } from '../hooks/useCheckout';
import { Text } from './Text';
import { Icon } from './Icons';
import { showConfetti } from '../utils';
import { trackEvent } from '../analytics';
const checkoutIds = {
monthly: '1601bc00-9623-435b-b1de-2a70a2445c13',
annual: 'aae95d78-05c8-46f5-b11e-2d40ddd211d3',
generic: 'd1d2da1a-ae8f-4222-bbaf-6e07da8954bf' //'f8c64e50-7734-438b-a122-3510156f14ed'
};
export function Pro({ user, onLoginClick, onBuyFromExtensionClick }) {
const hasCheckoutLoaded = useCheckout();
const [isAnnual, setIsAnnual] = useState(true);
useEffect(() => {
if (hasCheckoutLoaded) {
window.LemonSqueezy.Setup({
eventHandler: e => {
console.log('eventHandler', e);
if (e.event === 'Checkout.Success') {
showConfetti(2);
alertsService.add(
'You are now PRO! 🎉. Reloading your superpowers...'
);
setTimeout(() => {
window.location.reload();
}, 2000);
}
}
});
window.LemonSqueezy.Refresh();
}
}, [hasCheckoutLoaded]);
let upgradeActionEl;
if (window.IS_EXTENSION) {
upgradeActionEl = (
<button
type="button"
className="btn btn--pro jc-c"
onClick={onBuyFromExtensionClick}
>
Upgrade to PRO
</button>
);
} else if (window.user) {
upgradeActionEl = (
<A
class="btn btn--pro lemonsqueezy-button d-f jc-c ai-c"
style="gap:0.2rem"
href={`https://web-maker.lemonsqueezy.com/checkout/buy/${checkoutIds.generic}?embed=1&checkout[custom][userId]=${user?.uid}`}
onClick={() => {
trackEvent('ui', 'buyBtnClick');
}}
>
Go PRO
</A>
);
} else {
upgradeActionEl = (
<button
type="button"
className="btn btn--pro jc-c"
onClick={onLoginClick}
>
Login & upgrade to PRO
</button>
);
}
return (
<VStack gap={2} align="stretch">
{/* <Stack justify="center">
<Switch
labels={['Monthly', 'Annually']}
checked={isAnnual}
showBothLabels={true}
onChange={e => {
setIsAnnual(e.target.checked);
}}
/>
</Stack> */}
<Stack gap={2} align="stretch">
<Card
price="Free"
subTitle="&nbsp;"
name="Starter"
features={[
'Unlimited private creations',
'1 Public creation',
'2 Files mode creations'
]}
/>
<Card
bg="#674dad"
price={'Starting $6/mo'}
subTitle="Annual & One-time pricing available"
name="Pro"
action={upgradeActionEl}
features={[
'Unlimited private creations',
'Unlimited public creations',
'Unlimited files mode creations',
'Asset hosting',
'Priority support',
'No Ads'
]}
/>
</Stack>
<Stack justify="center">
<Text tag="p" appearance="secondary">
30 days refund policy if not satisfied.
</Text>
</Stack>
</VStack>
);
}
const Card = ({ bg, name, price, subTitle, action, features }) => {
return (
<div class="plan-card" style={{ background: bg }}>
<VStack gap={2} align="stretch" justify="flex-start">
<VStack gap={0} align="stretch" justify="flex-start">
<Text transform="uppercase" weight="600">
{name}
</Text>
<Text size="5" weight="800" appearance="primary">
{price}
</Text>
<Text size="1" weight="400">
{subTitle}
</Text>
</VStack>
{action}
{!action && (
<a class="btn" aria-hidden="true" style="visibility:hidden">
s
</a>
)}
<VStack gap={1} align="flex-start">
{features.map(f => (
<HStack gap={1} align="center">
<Icon name="check-circle" size="1.4rem" />
{f}
</HStack>
))}
</VStack>
</VStack>
</div>
);
};

View File

@@ -0,0 +1,3 @@
export const ProBadge = () => {
return <div className="badge pro-badge">PRO</div>;
};

View File

@@ -0,0 +1,31 @@
import { h } from 'preact';
import Modal from './Modal';
import { Text } from './Text';
import { Stack } from './Stack';
export function ProOnAppModal({ show, closeHandler }) {
return (
<Modal extraClasses="" show={show} closeHandler={closeHandler}>
<div class="ta">
<Text appearance="primary" tag="h1" size="4" weight="700">
Upgrade on Web app
</Text>
<p className="para">
Upgrading to PRO from the extension is not available yet. You can
instead upgrade on the Web app, come back here, refresh and get all
the PRO features right here!
</p>
<Stack justify="center">
<a
href="https://webmaker.app/create"
className="btn btn--pro"
target="_blank"
>
Upgrade on Web app
</a>
</Stack>
</div>
</Modal>
);
}

View File

@@ -1,30 +1,158 @@
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { ProBadge } from './ProBadge';
import { HStack, Stack, VStack } from './Stack';
import { Panel } from './Panel';
import { Text } from './Text';
import { getHumanReadableDate } from '../utils';
import { LoaderWithText } from './Loader';
const DEFAULT_PROFILE_IMG =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z'/%3E%3C/svg%3E";
export function Profile({ user, logoutBtnHandler }) {
const Header = ({ user, logoutBtnHandler }) => {
return (
<div class="tac">
<img
height="80"
class="profile-modal__avatar-img"
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
id="profileAvatarImg"
alt="Profile image"
/>
<h3 id="profileUserName" class="mb-2">
{user && user.displayName ? user.displayName : 'Anonymous Creator'}
</h3>
<p>
<button
class="btn"
aria-label="Logout from your account"
onClick={logoutBtnHandler}
>
Logout
</button>
</p>
</div>
<Stack gap={5}>
<Stack gap={2} align="center">
<img
height="80"
class={`profile-modal__avatar-img ${user?.isPro ? 'is-pro' : ''}`}
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
id="profileAvatarImg"
alt="Profile image"
/>
<VStack gap={1} align="flex-start">
<h3
class={`profile-modal__name ${user?.isPro ? 's-pro' : ''}`}
id="profileUserName"
>
{user && user.displayName ? user.displayName : 'Anonymous Creator'}
</h3>
{user.isPro && <ProBadge />}
</VStack>
</Stack>
<button
class="btn btn--primary"
aria-label="Logout from your account"
onClick={logoutBtnHandler}
>
Logout
</button>
</Stack>
);
};
export function Profile({ user, logoutBtnHandler }) {
const [currentSubscription, setCurrentSubscription] = useState(null);
const [isFetchingSubscription, setIsFetchingSubscription] = useState(false);
useEffect(() => {
if (user?.isPro) {
setIsFetchingSubscription(true);
window.db.getUserSubscriptionEvents(user.uid).then(events => {
setIsFetchingSubscription(false);
let creationEvent = events
.filter(
event =>
event.type === 'subscription_created' ||
event.type === 'order_created'
)
.sort((a, b) => b.timestamp.seconds - a.timestamp.seconds)
// remove order_created events which correspond to subscriptions (non lifetime orders)
.filter(
event =>
!(
event.type === 'order_created' &&
!event.data.data.attributes.first_order_item?.variant_name?.match(
/lifetime/i
)
)
)[0];
if (creationEvent) {
// log(creationEvent);
creationEvent.attributes = creationEvent.data.data.attributes;
setCurrentSubscription(creationEvent);
}
});
}
}, [user]);
return (
<VStack gap={4}>
<Header user={user} logoutBtnHandler={logoutBtnHandler} />
{window.user?.isPro && (
<Panel>
{isFetchingSubscription ? (
<LoaderWithText>Loading billing details...</LoaderWithText>
) : null}
{currentSubscription ? (
<VStack align="stretch" gap={1}>
<Text>
Plan:
<Text weight="700">
{' '}
Web Maker PRO (
{currentSubscription.attributes.variant_name ||
currentSubscription.attributes.first_order_item
?.variant_name}
)
</Text>
</Text>
<Text>
Subscription Status:{' '}
<Text weight="700">
{currentSubscription.attributes.status === 'paid'
? 'PRO for life ❤️'
: currentSubscription.attributes.status}
</Text>
</Text>
<Text>
Renews on:{' '}
<Text weight="700">
{currentSubscription.attributes.status === 'paid'
? 'Never ever'
: getHumanReadableDate(
currentSubscription.attributes.renews_at
)}
</Text>
</Text>
{currentSubscription.attributes.status === 'paid' ? null : (
<a
target="_blank"
href={currentSubscription.attributes.urls.customer_portal}
>
Cancel subscription
</a>
)}
{/* <a
target="_blank"
href={
currentSubscription.attributes.urls
.customer_portal_update_subscription
}
>
Link 2
</a>
<a
target="_blank"
href={currentSubscription.attributes.urls.update_payment_method}
>
Link 3
</a> */}
</VStack>
) : null}
</Panel>
)}
{user?.isPro && currentSubscription ? (
<img
class="profile-modal__panda"
src="assets/pro-panda.png"
width="300"
style="position:absolute;bottom:-3rem; right: -7rem;"
/>
) : null}
</VStack>
);
}

View File

@@ -157,18 +157,29 @@ export default function SavedItemPane({
>
<button
onClick={onCloseIntent}
class="btn saved-items-pane__close-btn"
class="btn dialog__close-btn saved-items-pane__close-btn"
id="js-saved-items-pane-close-btn"
aria-label={i18n._(t`Close saved creations pane`)}
>
X
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<div
class="flex flex-v-center"
style="justify-content: space-between;"
>
<h3>
<Trans>My Library ({filteredItems.length})</Trans>
<Trans>My Library</Trans> ({filteredItems.length})
</h3>
<div>
@@ -215,6 +226,9 @@ export default function SavedItemPane({
onClick={() => itemClickHandler(item)}
onForkBtnClick={e => itemForkBtnClickHandler(item, e)}
onRemoveBtnClick={e => itemRemoveBtnClickHandler(item, e)}
onToggleVisibilityBtnClick={e =>
itemVisibilityToggleHandler(item, e)
}
/>
))}
{!items.length ? (
@@ -224,7 +238,7 @@ export default function SavedItemPane({
</h2>
<img
style="max-width: 80%; opacity:0.4"
src="assets/empty.svg"
src="./assets/empty.svg"
/>
</div>
) : null}

149
src/components/Share.jsx Normal file
View File

@@ -0,0 +1,149 @@
import { useEffect, useState } from 'preact/hooks';
import { Skeleton } from './Skeleton';
import { HStack, Stack, VStack } from './Stack';
import Switch from './Switch';
import { itemService } from '../itemService';
import { alertsService } from '../notifications';
import { Button } from './common';
import { Icon } from './Icons';
import { Text } from './Text';
const FREE_PUBLIC_ITEM_COUNT = 1;
const BASE_URL = location.origin;
const TOGGLE_VISIBILITY_API =
/*!window.location.origin.includes('localhost')
? 'http://127.0.0.1:5001/web-maker-app/us-central1/toggleVisibility'
: */ 'https://togglevisibility-ajhkrtmkaq-uc.a.run.app';
export function Share({
user,
item,
onVisibilityChange,
onLoginBtnClick,
onProBtnClick
}) {
const [publicItemCount, setPublicItemCount] = useState();
useEffect(() => {
if (!user) return;
window.db.getPublicItemCount(user.uid).then(c => {
setPublicItemCount(c);
// console.log('public items', c);
});
}, []);
const [val, setVal] = useState(item.isPublic);
const onChange = async e => {
const newVal = e.target.checked;
setVal(newVal);
if (newVal) {
const token = await window.user.firebaseUser.getIdToken();
let res;
try {
res = await fetch(
`${TOGGLE_VISIBILITY_API}?token=${token}&itemId=${item.id}`
);
} catch (e) {
alertsService.add('Could not set visiblity to public');
setTimeout(() => {
setVal(!newVal);
}, 400);
return;
}
if (res.status >= 200 && res.status < 400) {
setPublicItemCount(publicItemCount + 1);
onVisibilityChange(true);
alertsService.add('Visiblity set to public');
} else {
alertsService.add('Could not set visiblity to public');
setTimeout(() => {
setVal(!newVal);
}, 400);
}
} else {
itemService.setItem(item.id, { isPublic: false });
setPublicItemCount(publicItemCount - 1);
onVisibilityChange(false);
alertsService.add('Visiblity set to private');
}
};
const copyUrl = () => {
navigator.clipboard.writeText(`${BASE_URL}/create/${item.id}`);
alertsService.add('URL copied to clipboard');
};
if (!user) {
return (
<HStack justify="center" gap={2}>
<Text>Login to share this creation publicly</Text>
<Button class="btn btn--primary" onClick={onLoginBtnClick}>
Login
</Button>
</HStack>
);
}
return (
<VStack gap={4} align="stretch">
<div style="min-width: 46ch">
<VStack gap={1} align="stretch">
<Switch
checked={val}
onChange={onChange}
labels={['Private', 'Public']}
>
Access
</Switch>
{item.isPublic && (
<p>
Public at{' '}
<a href={`${BASE_URL}/create/${item.id}`} target="_blank">
{BASE_URL}/create/{item.id}
</a>{' '}
<Button
class="btn btn--dark hint--bottom hint--rounded"
onClick={copyUrl}
aria-label="Copy"
>
<Icon name="copy" />
</Button>
</p>
)}
</VStack>
</div>
{!user?.isPro ? (
<VStack gap={1} align="stretch">
<p>
<HStack gap={3} justify="flex-start">
<span>Public creations available: {FREE_PUBLIC_ITEM_COUNT}</span>
<span>
Used:{' '}
{publicItemCount === undefined ? (
<Skeleton width="2ch" />
) : (
publicItemCount
)}
</span>
<span>
Left:{' '}
{publicItemCount === undefined ? (
<Skeleton width="2ch" />
) : (
Math.max(0, FREE_PUBLIC_ITEM_COUNT - publicItemCount)
)}
</span>
</HStack>
</p>
<p>
<HStack gap={1}>
<span>For unlimited public creations, </span>
<button onClick={onProBtnClick} class="btn btn--pro btn--small">
Upgrade to Pro
</button>
</HStack>
</p>
</VStack>
) : null}
</VStack>
);
}

View File

@@ -0,0 +1,3 @@
export const Skeleton = ({ width }) => {
return <div className="skeleton" style={{ width }}></div>;
};

54
src/components/Stack.jsx Normal file
View File

@@ -0,0 +1,54 @@
const gaps = [0, '0.5rem', '1rem', '1.5rem', '3rem', '5rem'];
const Stack = function ({
classes = '',
gap = 0,
align = 'center',
justify = 'flex-start',
direction = 'horizontal',
fullWidth = false,
fullHeight = false,
wrap,
children
}) {
return (
<div
style={{
display: 'flex',
gap: gaps[gap] || gap,
alignItems: align,
justifyContent: justify,
flexDirection: direction === 'vertical' ? 'column' : 'row',
height: fullHeight ? '100%' : null,
width: fullWidth ? '100%' : null,
flexWrap: wrap ? 'wrap' : null
}}
class={`stack ${classes}`}
>
{children}
</div>
);
};
const VStack = props => {
return <Stack {...props} direction="vertical" />;
};
const HStack = props => {
return (
<Stack
classes={`hstack ${props.responsive ? 'hstack--responsive' : ''}`}
{...props}
/>
);
};
const Spacer = () => {
return (
<>
<div style={{ flexGrow: '1' }}></div>
</>
);
};
export { Stack, VStack, HStack, Spacer };

View File

@@ -1,5 +1,3 @@
import { h } from 'preact';
export default function Switch({
checked,
onChange,

68
src/components/Text.jsx Normal file
View File

@@ -0,0 +1,68 @@
import { forwardRef } from 'preact/compat';
const appearanceStyles = {
normal: {
color: 'var(--color-text)'
},
primary: {
color: 'var(--color-heading)'
},
secondary: {
color: 'var(--color-text-light)'
},
tertiary: {
color: 'var(--color-text-lightest-final)'
},
brand: {
color: 'var(--color-brand)'
}
};
const sizes = {
0: '0.875rem',
1: '1rem',
2: '1.125rem',
3: '1.25rem',
4: '1.5rem',
5: '2rem',
6: '2.5rem',
7: '3rem',
8: '4rem'
};
export const Text = forwardRef(
(
{
size = 1,
weight = 'normal',
tag,
style = 'normal',
appearance = 'normal',
letterSpacing = 0,
lineHeight = 1.4,
align = 'left',
transform,
classes = '',
children
},
ref
) => {
const Tag = tag || 'span';
const styles = {
letterSpacing: letterSpacing,
fontSize: sizes[size],
textTransform: transform,
fontWeight: weight,
textAlign: align,
lineHeight: lineHeight,
fontStyle: style === 'italic' ? 'italic' : 'normal',
...appearanceStyles[appearance]
};
return (
<Tag style={styles} className={classes} ref={ref}>
{children}
</Tag>
);
}
);

View File

@@ -2,11 +2,12 @@
*/
import { h, Component } from 'preact';
import { route } from 'preact-router';
// import '../service-worker-registration';
import { MainHeader } from './MainHeader.jsx';
import ContentWrap from './ContentWrap.jsx';
import ContentWrapFiles from './ContentWrapFiles.jsx';
import Footer from './Footer.jsx';
import { Footer } from './Footer.jsx';
import SavedItemPane from './SavedItemPane.jsx';
import AddLibrary from './AddLibrary.jsx';
import Modal from './Modal.jsx';
@@ -22,7 +23,8 @@ import {
getCompleteHtml,
getFilenameFromUrl,
prettify,
sanitizeSplitSizes
sanitizeSplitSizes,
persistAuthUserLocally
} from '../utils';
import {
linearizeFiles,
@@ -35,7 +37,7 @@ import {
import { itemService } from '../itemService';
import '../db';
import { Notifications } from './Notifications';
import { Changelog } from './Changelog';
import Settings from './Settings.jsx';
import { modes, HtmlModes, CssModes, JsModes } from '../codeModes';
import { trackEvent } from '../analytics';
@@ -68,14 +70,21 @@ import {
import { commandPaletteService } from '../commandPaletteService';
import { I18nProvider } from '@lingui/react';
import { Assets } from './Assets.jsx';
import { LocalStorageKeys } from '../constants.js';
import { Share } from './Share.jsx';
import { Pro } from './Pro.jsx';
import { VStack } from './Stack.jsx';
import { ProBadge } from './ProBadge.jsx';
import { Text } from './Text.jsx';
import { ProOnAppModal } from './ProOnAppModal.js';
if (module.hot) {
require('preact/debug');
}
const UNSAVED_WARNING_COUNT = 15;
const version = '5.2.0';
const version = '6.1.0';
// Read forced settings as query parameters
window.forcedSettings = {};
@@ -98,10 +107,19 @@ if (location.search) {
window.codeCss = params.get('css') || '';
}
function customRoute(path) {
// we don't /create redirections on extension since SPA paths don't work there
if (window.IS_EXTENSION) return;
else {
route(path);
}
}
export default class App extends Component {
constructor() {
super();
this.AUTO_SAVE_INTERVAL = 15000; // 15 seconds
const savedUser = window.localStorage.getItem('user');
this.modalDefaultStates = {
isModalOpen: false,
isAddLibraryModalOpen: false,
@@ -116,7 +134,12 @@ export default class App extends Component {
isOnboardModalOpen: false,
isJs13KModalOpen: false,
isCreateNewModalOpen: false,
isCommandPaletteOpen: false
isCommandPaletteOpen: false,
isAssetsOpen: false,
isShareModalOpen: false,
isProModalOpen: false,
isFilesLimitModalOpen: false,
isProOnAppModalOpen: false
};
this.state = {
isSavedItemPaneOpen: false,
@@ -128,7 +151,8 @@ export default class App extends Component {
html: window.codeHtml,
css: window.codeCss
},
catalogs: {}
catalogs: {},
user: savedUser
};
this.defaultSettings = {
preserveLastCode: true,
@@ -161,13 +185,35 @@ export default class App extends Component {
};
this.prefs = {};
onAuthStateChanged(auth, user => {
if (savedUser) {
window.user = savedUser;
}
onAuthStateChanged(auth, authUser => {
this.setState({ isLoginModalOpen: false });
if (user) {
log('You are -> ', user);
if (authUser) {
log('You are -> ', authUser);
alertsService.add('You are now logged in!');
this.setState({ user });
window.user = user;
let newUser = {
uid: authUser.uid,
photoURL: authUser.photoURL
};
// port some keys from localstorage user to new auth user
const keysToPort = ['isPro', 'displayName', 'settings'];
keysToPort.forEach(key => {
if (this.state.user && this.state.user[key] !== undefined) {
newUser[key] = this.state.user[key];
}
});
// storing actual firebase user object for accessing functions like updateProfile
newUser.firebaseUser = authUser;
this.setState({ user: newUser });
window.user = newUser;
// window.localStorage.setItem('user', authUser);
trackEvent('fn', 'loggedIn', window.IS_EXTENSION ? 'extension' : 'web');
if (!window.localStorage[LocalStorageKeys.ASKED_TO_IMPORT_CREATIONS]) {
this.fetchItems(false, true).then(items => {
if (!items.length) {
@@ -181,12 +227,23 @@ export default class App extends Component {
trackEvent('ui', 'askToImportModalSeen');
});
}
window.db.getUser(user.uid).then(customUser => {
window.db.getUser(authUser.uid).then(customUser => {
if (customUser) {
const prefs = { ...this.state.prefs };
Object.assign(prefs, user.settings);
this.setState({ prefs }, this.updateSetting);
Object.assign(prefs, customUser.settings);
// spreading authUser doesn't work below anymore because the required properties are
// not enumerable anymore
newUser = {
...newUser,
isPro: false,
...customUser
};
window.user = newUser;
this.setState({ user: newUser, prefs }, this.updateSetting);
}
persistAuthUserLocally(newUser);
});
} else {
// User is signed out.
@@ -234,6 +291,18 @@ export default class App extends Component {
this.setCurrentItem(this.state.currentItem).then(() => {
this.refreshEditor();
});
} else if (this.props.itemId) {
window.db
.fetchItem(this.props.itemId)
.then(item => {
this.setCurrentItem(item).then(() => this.refreshEditor());
})
.catch(err => {
alert('No such creation found!');
this.createNewItem();
// route('/');
});
} else if (result.preserveLastCode && lastCode) {
this.setState({ unsavedEditCount: 0 });
log('Load last unsaved item', lastCode);
@@ -267,8 +336,8 @@ export default class App extends Component {
semverCompare(lastSeenVersion, version) === -1 &&
!window.localStorage.pledgeModalSeen
) {
this.openSupportDeveloperModal();
window.localStorage.pledgeModalSeen = true;
// this.openSupportDeveloperModal();
// window.localStorage.pledgeModalSeen = true;
}
if (!lastSeenVersion || semverCompare(lastSeenVersion, version) === -1) {
@@ -346,9 +415,12 @@ export default class App extends Component {
}
const fork = JSON.parse(JSON.stringify(sourceItem));
delete fork.id;
delete fork.createdBy;
delete fork.isPublic;
fork.title = '(Forked) ' + sourceItem.title;
fork.updatedOn = Date.now();
this.setCurrentItem(fork).then(() => this.refreshEditor());
customRoute('/create');
alertsService.add(`"${sourceItem.title}" was forked`);
trackEvent('fn', 'itemForked');
}
@@ -408,10 +480,12 @@ export default class App extends Component {
};
}
this.setCurrentItem(item).then(() => this.refreshEditor());
customRoute('/create');
alertsService.add('New item created');
}
openItem(item) {
this.setCurrentItem(item).then(() => this.refreshEditor());
customRoute(`/create/${item.id}`);
alertsService.add('Saved item loaded');
}
removeItem(item) {
@@ -538,6 +612,9 @@ export default class App extends Component {
openAddLibrary() {
this.setState({ isAddLibraryModalOpen: true });
}
assetsBtnClickHandler() {
this.setState({ isAssetsOpen: true });
}
closeSavedItemsPane() {
this.setState({
isSavedItemPaneOpen: false
@@ -556,6 +633,7 @@ export default class App extends Component {
}
componentDidMount() {
console.log('itemId', this.props.itemId);
function setBodySize() {
document.body.style.height = `${window.innerHeight}px`;
}
@@ -891,8 +969,16 @@ export default class App extends Component {
trackEvent('ui', LocalStorageKeys.LOGIN_AND_SAVE_MESSAGE_SEEN, 'local');
}
var isNewItem = !this.state.currentItem.id;
this.state.currentItem.id =
this.state.currentItem.id || 'item-' + generateRandomId();
this.state.currentItem.id = this.state.currentItem.id || generateRandomId();
if (
this.state.currentItem.createdBy &&
this.state.currentItem.createdBy !== this.state.user.uid
) {
alertsService.add(
'You cannot save this item as it was created by someone else. Fork it to save it as your own.'
);
return;
}
this.setState({
isSaving: true
});
@@ -952,14 +1038,17 @@ export default class App extends Component {
}
titleInputBlurHandler(e) {
this.setState({
currentItem: { ...this.state.currentItem, title: e.target.value }
});
if (this.state.currentItem.id) {
this.saveItem();
trackEvent('ui', 'titleChanged');
}
this.setState(
{
currentItem: { ...this.state.currentItem, title: e.target.value }
},
() => {
if (this.state.currentItem.id) {
this.saveItem();
trackEvent('ui', 'titleChanged');
}
}
);
}
/**
@@ -1045,6 +1134,7 @@ export default class App extends Component {
trackEvent('fn', 'loggedOut');
authh.logout();
this.setState({ isProfileModalOpen: false });
this.createNewItem();
alertsService.add('Log out successfull');
}
@@ -1087,6 +1177,14 @@ export default class App extends Component {
trackEvent('ui', 'openBtnClick');
this.openSavedItemsPane();
}
shareBtnClickHandler() {
trackEvent('ui', 'shareBtnClick');
if (!window.user || this.state.currentItem.id) {
this.setState({ isShareModalOpen: true });
} else {
alertsService.add('Please save your creation before sharing.');
}
}
detachedPreviewBtnHandler() {
trackEvent('ui', 'detachPreviewBtnClick');
@@ -1095,7 +1193,7 @@ export default class App extends Component {
notificationsBtnClickHandler() {
this.setState({ isNotificationsModalOpen: true });
if (this.state.isNotificationsModalOpen && !this.hasSeenNotifications) {
if (!this.state.isNotificationsModalOpen && !this.hasSeenNotifications) {
this.hasSeenNotifications = true;
this.setState({ hasUnseenChangelog: false });
window.db.setUserLastSeenVersion(version);
@@ -1103,6 +1201,15 @@ export default class App extends Component {
trackEvent('ui', 'notificationButtonClick', version);
return false;
}
proBtnClickHandler() {
if (this.state.user?.isPro) {
this.setState({ isProfileModalOpen: true });
trackEvent('ui', 'manageProBtnClick');
} else {
this.setState({ isProModalOpen: true });
trackEvent('ui', 'proBtnClick');
}
}
codepenBtnClickHandler(e) {
if (this.state.currentItem.cssMode === CssModes.ACSS) {
alert(
@@ -1443,9 +1550,11 @@ export default class App extends Component {
this.setState({ isCreateNewModalOpen: false });
} else {
trackEvent('ui', 'FileModeCreationLimitMessageSeen');
return alert(
'"Files mode" is currently in beta and is limited to only 2 creations per user. You have already made 2 creations in Files mode.\n\nNote: You can choose to delete old ones to create new.'
);
// this.closeAllOverlays();
this.setState({ isFilesLimitModalOpen: true });
// return alert(
// '"Files mode" is currently in beta and is limited to only 2 creations per user. You have already made 2 creations in Files mode.\n\nNote: You can choose to delete old ones to create new.'
// );
}
});
}
@@ -1578,7 +1687,7 @@ export default class App extends Component {
// 3 pane mode
if (typeof what === 'string') {
prettify({
content: this.state.currentItem[what],
content: this.state.currentItem[what] || '',
type: { html: 'html', js: 'js', css: 'css' }[what]
}).then(formattedContent => {
if (this.state.currentItem[what] === formattedContent) {
@@ -1599,6 +1708,7 @@ export default class App extends Component {
...this.state.currentItem,
files: [...this.state.currentItem.files]
};
prettify({ file: selectedFile }).then(formattedContent => {
if (formattedContent !== selectedFile.content) {
selectedFile.content = formattedContent;
@@ -1621,10 +1731,12 @@ export default class App extends Component {
loginBtnHandler={this.loginBtnClickHandler.bind(this)}
profileBtnHandler={this.profileBtnClickHandler.bind(this)}
addLibraryBtnHandler={this.openAddLibrary.bind(this)}
assetsBtnHandler={this.assetsBtnClickHandler.bind(this)}
shareBtnHandler={this.shareBtnClickHandler.bind(this)}
runBtnClickHandler={this.runBtnClickHandler.bind(this)}
isFetchingItems={this.state.isFetchingItems}
isSaving={this.state.isSaving}
title={this.state.currentItem.title}
currentItem={this.state.currentItem}
titleInputBlurHandler={this.titleInputBlurHandler.bind(this)}
user={this.state.user}
isAutoPreviewOn={this.state.prefs.autoPreview}
@@ -1632,6 +1744,9 @@ export default class App extends Component {
isFileMode={
this.state.currentItem && this.state.currentItem.files
}
onItemFork={() => {
this.forkItem(this.state.currentItem);
}}
/>
{this.state.currentItem && this.state.currentItem.files ? (
<ContentWrapFiles
@@ -1667,6 +1782,7 @@ export default class App extends Component {
<Footer
prefs={this.state.prefs}
user={this.state.user}
layoutBtnClickHandler={this.layoutBtnClickHandler.bind(this)}
helpBtnClickHandler={() =>
this.setState({ isHelpModalOpen: true })
@@ -1693,11 +1809,11 @@ export default class App extends Component {
onJs13KDownloadBtnClick={this.js13KDownloadBtnClickHandler.bind(
this
)}
proBtnClickHandler={this.proBtnClickHandler.bind(this)}
hasUnseenChangelog={this.state.hasUnseenChangelog}
codeSize={this.state.codeSize}
/>
</div>
<SavedItemPane
itemsMap={this.state.savedItems}
isOpen={this.state.isSavedItemPaneOpen}
@@ -1708,9 +1824,7 @@ export default class App extends Component {
onExport={this.exportBtnClickHandler.bind(this)}
mergeImportedItems={this.mergeImportedItems.bind(this)}
/>
<Alerts />
<Modal
show={this.state.isAddLibraryModalOpen}
closeHandler={() => this.setState({ isAddLibraryModalOpen: false })}
@@ -1735,7 +1849,7 @@ export default class App extends Component {
this.setState({ isNotificationsModalOpen: false })
}
>
<Notifications
<Changelog
onSupportBtnClick={this.openSupportDeveloperModal.bind(this)}
/>
</Modal>
@@ -1749,13 +1863,6 @@ export default class App extends Component {
onChange={this.updateSetting.bind(this)}
/>
</Modal>
<Modal
extraClasses="login-modal"
show={this.state.isLoginModalOpen}
closeHandler={() => this.setState({ isLoginModalOpen: false })}
>
<Login />
</Modal>
<Modal
show={this.state.isProfileModalOpen}
closeHandler={() => this.setState({ isProfileModalOpen: false })}
@@ -1765,6 +1872,73 @@ export default class App extends Component {
logoutBtnHandler={this.logout.bind(this)}
/>
</Modal>
<Modal
show={this.state.isAssetsOpen}
closeHandler={() => this.setState({ isAssetsOpen: false })}
>
<Assets
onProBtnClick={() => {
this.setState({ isAssetsOpen: false });
this.proBtnClickHandler();
}}
onLoginBtnClick={() => {
this.closeAllOverlays();
this.loginBtnClickHandler();
}}
/>
</Modal>
<Modal
show={this.state.isShareModalOpen}
closeHandler={() => this.setState({ isShareModalOpen: false })}
>
<Share
user={this.state.user}
item={this.state.currentItem}
onVisibilityChange={visibility => {
const item = {
...this.state.currentItem,
isPublic: visibility
};
this.setState({ currentItem: item });
}}
onLoginBtnClick={() => {
this.closeAllOverlays();
this.loginBtnClickHandler();
}}
onProBtnClick={() => {
this.closeAllOverlays();
this.proBtnClickHandler();
}}
/>
</Modal>
<Modal
show={this.state.isProModalOpen}
closeHandler={() => this.setState({ isProModalOpen: false })}
extraClasses="pro-modal"
>
<Pro
user={this.state.user}
onLoginClick={() => {
this.closeAllOverlays();
this.loginBtnClickHandler();
}}
onBuyFromExtensionClick={() => {
console.log('open modal');
this.closeAllOverlays();
this.setState({ isProOnAppModalOpen: true });
}}
/>
</Modal>
{/* Login modal is intentionally kept here after assets & share modal because
they trigger this modal and if order isn't maintainer, the modal overlay doesn't
show properly */}
<Modal
extraClasses="login-modal"
show={this.state.isLoginModalOpen}
closeHandler={() => this.setState({ isLoginModalOpen: false })}
>
<Login />
</Modal>
<HelpModal
show={this.state.isHelpModalOpen}
closeHandler={() => this.setState({ isHelpModalOpen: false })}
@@ -1794,17 +1968,14 @@ export default class App extends Component {
)}
dontAskBtnClickHandler={this.dontAskToImportAnymore.bind(this)}
/>
<OnboardingModal
show={this.state.isOnboardModalOpen}
closeHandler={() => this.setState({ isOnboardModalOpen: false })}
/>
<Js13KModal
show={this.state.isJs13KModalOpen}
closeHandler={() => this.setState({ isJs13KModalOpen: false })}
/>
<CreateNewModal
show={this.state.isCreateNewModalOpen}
closeHandler={() => this.setState({ isCreateNewModalOpen: false })}
@@ -1817,21 +1988,38 @@ export default class App extends Component {
this
)}
/>
<ProOnAppModal
show={this.state.isProOnAppModalOpen}
closeHandler={() => this.setState({ isProOnAppModalOpen: false })}
/>
<Modal
extraClasses=""
show={this.state.isFilesLimitModalOpen}
closeHandler={() => this.setState({ isFilesLimitModalOpen: false })}
>
<VStack align="stretch" gap={2}>
<Text tag="p">
You have used your quota of 2 'Files mode' creations in Free
plan.
</Text>
<Text tag="p">
You can choose to delete old ones to free quota or upgrade to{' '}
<ProBadge />.{' '}
</Text>
</VStack>
</Modal>
<CommandPalette
show={this.state.isCommandPaletteOpen}
files={linearizeFiles(this.state.currentItem.files || [])}
isCommandMode={this.state.isCommandPaletteInCommandMode}
closeHandler={() => this.setState({ isCommandPaletteOpen: false })}
/>
<Portal into="#portal">
<div
class="modal-overlay"
onClick={this.modalOverlayClickHandler.bind(this)}
/>
</Portal>
<Icons />
<form
style="display:none;"

View File

@@ -4,11 +4,15 @@ import { trackEvent } from '../analytics';
class Clickable extends Component {
handleClick(e) {
const el = e.currentTarget;
trackEvent(
el.getAttribute('data-event-category'),
el.getAttribute('data-event-action')
);
this.props.onClick(e);
if (el.getAttribute('data-event-category')) {
trackEvent(
el.getAttribute('data-event-category'),
el.getAttribute('data-event-action')
);
}
if (this.props.onClick) {
this.props.onClick(e);
}
}
render() {
/* eslint-disable no-unused-vars */
@@ -38,5 +42,5 @@ export function Divider(props) {
}
export function BetaTag() {
return <span class="beta-tag">Beta</span>;
return <span class="badge beta-tag">Beta</span>;
}

View File

@@ -6,6 +6,24 @@ import { deferred } from './deferred';
import { trackEvent } from './analytics';
import { log } from './utils';
/**
* Converts a firestore query snapshot into native array
* @param {snapshot} querySnapshot Snapshot object returned by a firestore query
*/
function getArrayFromQuerySnapshot(querySnapshot) {
const arr = [];
querySnapshot.forEach(doc => {
// doc.data() has to be after doc.id because docs can have `id` key in them which
// should override the explicit `id` being set
arr.push({
id: doc.id,
...doc.data()
});
// documentCache[doc.id] = doc.data()
});
return arr;
}
(() => {
const FAUX_DELAY = 1;
@@ -129,15 +147,28 @@ import { log } from './utils';
return getDoc(doc(remoteDb, `users/${userId}`)).then(doc => {
if (!doc.exists()) {
return setDoc(doc(remoteDb, `users/${userId}`), {}, { merge: true });
// return setDoc(doc(remoteDb, `users/${userId}`), {}, { merge: true });
return {};
}
const user = doc.data();
Object.assign(window.user, user);
// Object.assign(window.user, user);
return user;
});
}
async function fetchItem(itemId) {
const remoteDb = await getDb();
return remoteDb
.doc(`items/${itemId}`)
.get()
.then(doc => {
if (!doc.exists) return {};
const data = doc.data();
return data;
});
}
// Fetch user settings.
// This isn't hitting the remote db because remote settings
// get fetch asynchronously (in user/) and update the envioronment.
@@ -151,12 +182,36 @@ import { log } from './utils';
return d.promise;
}
async function getPublicItemCount(userId) {
const remoteDb = await getDb();
return remoteDb
.collection('items')
.where('createdBy', '==', userId)
.where('isPublic', '==', true)
.get()
.then(snapShot => {
return snapShot.size;
});
}
async function getUserSubscriptionEvents(userId) {
const remoteDb = await getDb();
return remoteDb
.collection('subscriptions')
.where('userId', '==', userId)
.get()
.then(getArrayFromQuerySnapshot);
}
window.db = {
getDb,
getUser,
getUserLastSeenVersion,
setUserLastSeenVersion,
getSettings,
fetchItem,
getPublicItemCount,
getUserSubscriptionEvents,
local: dbLocalAlias,
sync: dbSyncAlias
};

20
src/hooks/useCheckout.js Normal file
View File

@@ -0,0 +1,20 @@
import { useState, useEffect } from 'react';
function useCheckout() {
const [hasVendorScriptLoaded, setHasVendorScriptLoaded] = useState();
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://app.lemonsqueezy.com/js/lemon.js';
script.async = 'true';
script.defer = 'true';
script.addEventListener('load', () => {
window.createLemonSqueezy();
setHasVendorScriptLoaded(true);
});
document.body.appendChild(script);
}, []);
return hasVendorScriptLoaded;
}
export { useCheckout };

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
@@ -11,11 +11,12 @@
rel="manifest"
href="<%= htmlWebpackPlugin.files.publicPath %>manifest.json"
/>
<% if (htmlWebpackPlugin.options.manifest.theme_color) { %>
<meta
name="theme-color"
content="<%= htmlWebpackPlugin.options.manifest.theme_color %>"
/>
<% if (cli.env.isProd) { %>
<base href="/create/" />
<% } else { %>
<base href="/" />
<% } %> <% if (cli.manifest.theme_color) { %>
<meta name="theme-color" content="<%= cli.manifest.theme_color %>" />
<% } %>
<style>
@@ -38,7 +39,7 @@
<link
rel="stylesheet"
id="editorThemeLinkTag"
href="lib/codemirror/theme/monokai.css"
href="./lib/codemirror/theme/monokai.css"
/>
<style id="fontStyleTemplate" type="template">
@@ -73,13 +74,13 @@
<body>
<div id="root"></div>
<div id="portal"></div>
<!-- SCRIPT-TAGS -->
<%= htmlWebpackPlugin.options.ssr({ url: '/' }) %>
<script
defer
src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"
></script>
<!-- END-SCRIPT-TAGS -->
<%= cli.ssr %> <% if (cli.config.prerender === true) { %>
<script type="__PREACT_CLI_DATA__">
<%= encodeURI(JSON.stringify(cli.CLI_DATA)) %>
</script>
<% } %>
<script type="module" src="<%= cli.entrypoints['bundle'] %>"></script>
<script nomodule src="<%= cli.entrypoints['dom-polyfills'] %>"></script>
</body>
</html>

View File

@@ -1,3 +1,4 @@
import Router from 'preact-router';
import App from './components/app.jsx';
import './lib/codemirror/lib/codemirror.css';
@@ -8,4 +9,13 @@ import './lib/hint.min.css';
import './lib/inlet.css';
import './style.css';
export default App;
export default function () {
return (
<Router>
<App path="/" />
<App path="/create/:itemId" />
<App path="/app/create/:itemId" />
<App default />
</Router>
);
}

View File

@@ -1,6 +1,6 @@
<script>
function callback(e) {
console.log('post message recvd', e.data);
// console.log('post message recvd', e.data);
window.document.open();
const { contents } = e.data;

View File

@@ -59,8 +59,6 @@ export const itemService = {
if (window.user && !shouldFetchLocally) {
var remoteDb = await window.db.getDb();
// console.log(query, window.user.uid);
// d.resolve([]);
const q = query(
collection(remoteDb, 'items'),

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
/**
* Link to the project's GitHub page:
@@ -349,6 +349,10 @@ CodeMirror.defineMode("coffeescript", function(conf, parserConf) {
return external;
});
// IANA registered media type
// https://www.iana.org/assignments/media-types/
CodeMirror.defineMIME("application/vnd.coffeescript", "coffeescript");
CodeMirror.defineMIME("text/x-coffeescript", "coffeescript");
CodeMirror.defineMIME("text/coffeescript", "coffeescript");

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -28,7 +28,9 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
colorKeywords = parserConfig.colorKeywords || {},
valueKeywords = parserConfig.valueKeywords || {},
allowNested = parserConfig.allowNested,
supportsAtComponent = parserConfig.supportsAtComponent === true;
lineComment = parserConfig.lineComment,
supportsAtComponent = parserConfig.supportsAtComponent === true,
highlightNonStandardPropertyKeywords = config.highlightNonStandardPropertyKeywords !== false;
var type, override;
function ret(style, tp) { type = tp; return style; }
@@ -62,7 +64,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
if (/[\d.]/.test(stream.peek())) {
stream.eatWhile(/[\w.%]/);
return ret("number", "unit");
} else if (stream.match(/^-[\w\\\-]+/)) {
} else if (stream.match(/^-[\w\\\-]*/)) {
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ret("variable-2", "variable-definition");
@@ -76,12 +78,11 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
return ret("qualifier", "qualifier");
} else if (/[:;{}\[\]\(\)]/.test(ch)) {
return ret(null, ch);
} else if ((ch == "u" && stream.match(/rl(-prefix)?\(/)) ||
(ch == "d" && stream.match("omain(")) ||
(ch == "r" && stream.match("egexp("))) {
stream.backUp(1);
state.tokenize = tokenParenthesized;
return ret("property", "word");
} else if (stream.match(/^[\w-.]+(?=\()/)) {
if (/^(url(-prefix)?|domain|regexp)$/i.test(stream.current())) {
state.tokenize = tokenParenthesized;
}
return ret("variable callee", "variable");
} else if (/[\w\\\-]/.test(ch)) {
stream.eatWhile(/[\w\\\-]/);
return ret("property", "word");
@@ -107,7 +108,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
function tokenParenthesized(stream, state) {
stream.next(); // Must be '('
if (!stream.match(/\s*[\"\')]/, false))
if (!stream.match(/^\s*[\"\')]/, false))
state.tokenize = tokenString(")");
else
state.tokenize = null;
@@ -161,16 +162,16 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
return pushContext(state, stream, "block");
} else if (type == "}" && state.context.prev) {
return popContext(state);
} else if (supportsAtComponent && /@component/.test(type)) {
} else if (supportsAtComponent && /@component/i.test(type)) {
return pushContext(state, stream, "atComponentBlock");
} else if (/^@(-moz-)?document$/.test(type)) {
} else if (/^@(-moz-)?document$/i.test(type)) {
return pushContext(state, stream, "documentTypes");
} else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) {
} else if (/^@(media|supports|(-moz-)?document|import)$/i.test(type)) {
return pushContext(state, stream, "atBlock");
} else if (/^@(font-face|counter-style)/.test(type)) {
} else if (/^@(font-face|counter-style)/i.test(type)) {
state.stateArg = type;
return "restricted_atBlock_before";
} else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) {
} else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(type)) {
return "keyframes";
} else if (type && type.charAt(0) == "@") {
return pushContext(state, stream, "at");
@@ -197,7 +198,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
override = "property";
return "maybeprop";
} else if (nonStandardPropertyKeywords.hasOwnProperty(word)) {
override = "string-2";
override = highlightNonStandardPropertyKeywords ? "string-2" : "property";
return "maybeprop";
} else if (allowNested) {
override = stream.match(/^\s*:(?:\s|$)/, false) ? "property" : "tag";
@@ -227,7 +228,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
if (type == "}" || type == "{") return popAndPass(type, stream, state);
if (type == "(") return pushContext(state, stream, "parens");
if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) {
if (type == "hash" && !/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(stream.current())) {
override += " error";
} else if (type == "word") {
wordAsValue(stream);
@@ -253,6 +254,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
};
states.pseudo = function(type, stream, state) {
if (type == "meta") return "pseudo";
if (type == "word") {
override = "variable-3";
return state.context.type;
@@ -289,7 +292,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
else if (propertyKeywords.hasOwnProperty(word))
override = "property";
else if (nonStandardPropertyKeywords.hasOwnProperty(word))
override = "string-2";
override = highlightNonStandardPropertyKeywords ? "string-2" : "property";
else if (valueKeywords.hasOwnProperty(word))
override = "atom";
else if (colorKeywords.hasOwnProperty(word))
@@ -380,7 +383,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
style = style[0];
}
override = style;
state.state = states[state.state](type, stream, state);
if (type != "comment")
state.state = states[state.state](type, stream, state);
return override;
},
@@ -398,7 +402,6 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
ch == "{" && (cx.type == "at" || cx.type == "atBlock")) {
// Dedent relative to current context.
indent = Math.max(0, cx.indent - indentUnit);
cx = cx.prev;
}
}
return indent;
@@ -407,6 +410,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
electricChars: "}",
blockCommentStart: "/*",
blockCommentEnd: "*/",
blockCommentContinue: " * ",
lineComment: lineComment,
fold: "brace"
};
});
@@ -414,7 +419,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
function keySet(array) {
var keys = {};
for (var i = 0; i < array.length; ++i) {
keys[array[i]] = true;
keys[array[i].toLowerCase()] = true;
}
return keys;
}
@@ -438,117 +443,151 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"monochrome", "min-monochrome", "max-monochrome", "resolution",
"min-resolution", "max-resolution", "scan", "grid", "orientation",
"device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio",
"pointer", "any-pointer", "hover", "any-hover"
"pointer", "any-pointer", "hover", "any-hover", "prefers-color-scheme",
"dynamic-range", "video-dynamic-range"
], mediaFeatures = keySet(mediaFeatures_);
var mediaValueKeywords_ = [
"landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover",
"interlace", "progressive"
"interlace", "progressive",
"dark", "light",
"standard", "high"
], mediaValueKeywords = keySet(mediaValueKeywords_);
var propertyKeywords_ = [
"align-content", "align-items", "align-self", "alignment-adjust",
"alignment-baseline", "anchor-point", "animation", "animation-delay",
"alignment-baseline", "all", "anchor-point", "animation", "animation-delay",
"animation-direction", "animation-duration", "animation-fill-mode",
"animation-iteration-count", "animation-name", "animation-play-state",
"animation-timing-function", "appearance", "azimuth", "backface-visibility",
"background", "background-attachment", "background-blend-mode", "background-clip",
"background-color", "background-image", "background-origin", "background-position",
"background-repeat", "background-size", "baseline-shift", "binding",
"bleed", "bookmark-label", "bookmark-level", "bookmark-state",
"bookmark-target", "border", "border-bottom", "border-bottom-color",
"border-bottom-left-radius", "border-bottom-right-radius",
"border-bottom-style", "border-bottom-width", "border-collapse",
"border-color", "border-image", "border-image-outset",
"animation-timing-function", "appearance", "azimuth", "backdrop-filter",
"backface-visibility", "background", "background-attachment",
"background-blend-mode", "background-clip", "background-color",
"background-image", "background-origin", "background-position",
"background-position-x", "background-position-y", "background-repeat",
"background-size", "baseline-shift", "binding", "bleed", "block-size",
"bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target",
"border", "border-bottom", "border-bottom-color", "border-bottom-left-radius",
"border-bottom-right-radius", "border-bottom-style", "border-bottom-width",
"border-collapse", "border-color", "border-image", "border-image-outset",
"border-image-repeat", "border-image-slice", "border-image-source",
"border-image-width", "border-left", "border-left-color",
"border-left-style", "border-left-width", "border-radius", "border-right",
"border-right-color", "border-right-style", "border-right-width",
"border-spacing", "border-style", "border-top", "border-top-color",
"border-top-left-radius", "border-top-right-radius", "border-top-style",
"border-top-width", "border-width", "bottom", "box-decoration-break",
"box-shadow", "box-sizing", "break-after", "break-before", "break-inside",
"caption-side", "clear", "clip", "color", "color-profile", "column-count",
"column-fill", "column-gap", "column-rule", "column-rule-color",
"column-rule-style", "column-rule-width", "column-span", "column-width",
"columns", "content", "counter-increment", "counter-reset", "crop", "cue",
"cue-after", "cue-before", "cursor", "direction", "display",
"dominant-baseline", "drop-initial-after-adjust",
"drop-initial-after-align", "drop-initial-before-adjust",
"drop-initial-before-align", "drop-initial-size", "drop-initial-value",
"elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis",
"flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap",
"float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings",
"font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust",
"font-stretch", "font-style", "font-synthesis", "font-variant",
"font-variant-alternates", "font-variant-caps", "font-variant-east-asian",
"font-variant-ligatures", "font-variant-numeric", "font-variant-position",
"font-weight", "grid", "grid-area", "grid-auto-columns", "grid-auto-flow",
"grid-auto-position", "grid-auto-rows", "grid-column", "grid-column-end",
"grid-column-start", "grid-row", "grid-row-end", "grid-row-start",
"border-image-width", "border-left", "border-left-color", "border-left-style",
"border-left-width", "border-radius", "border-right", "border-right-color",
"border-right-style", "border-right-width", "border-spacing", "border-style",
"border-top", "border-top-color", "border-top-left-radius",
"border-top-right-radius", "border-top-style", "border-top-width",
"border-width", "bottom", "box-decoration-break", "box-shadow", "box-sizing",
"break-after", "break-before", "break-inside", "caption-side", "caret-color",
"clear", "clip", "color", "color-profile", "column-count", "column-fill",
"column-gap", "column-rule", "column-rule-color", "column-rule-style",
"column-rule-width", "column-span", "column-width", "columns", "contain",
"content", "counter-increment", "counter-reset", "crop", "cue", "cue-after",
"cue-before", "cursor", "direction", "display", "dominant-baseline",
"drop-initial-after-adjust", "drop-initial-after-align",
"drop-initial-before-adjust", "drop-initial-before-align", "drop-initial-size",
"drop-initial-value", "elevation", "empty-cells", "fit", "fit-content", "fit-position",
"flex", "flex-basis", "flex-direction", "flex-flow", "flex-grow",
"flex-shrink", "flex-wrap", "float", "float-offset", "flow-from", "flow-into",
"font", "font-family", "font-feature-settings", "font-kerning",
"font-language-override", "font-optical-sizing", "font-size",
"font-size-adjust", "font-stretch", "font-style", "font-synthesis",
"font-variant", "font-variant-alternates", "font-variant-caps",
"font-variant-east-asian", "font-variant-ligatures", "font-variant-numeric",
"font-variant-position", "font-variation-settings", "font-weight", "gap",
"grid", "grid-area", "grid-auto-columns", "grid-auto-flow", "grid-auto-rows",
"grid-column", "grid-column-end", "grid-column-gap", "grid-column-start",
"grid-gap", "grid-row", "grid-row-end", "grid-row-gap", "grid-row-start",
"grid-template", "grid-template-areas", "grid-template-columns",
"grid-template-rows", "hanging-punctuation", "height", "hyphens",
"icon", "image-orientation", "image-rendering", "image-resolution",
"inline-box-align", "justify-content", "left", "letter-spacing",
"line-break", "line-height", "line-stacking", "line-stacking-ruby",
"grid-template-rows", "hanging-punctuation", "height", "hyphens", "icon",
"image-orientation", "image-rendering", "image-resolution", "inline-box-align",
"inset", "inset-block", "inset-block-end", "inset-block-start", "inset-inline",
"inset-inline-end", "inset-inline-start", "isolation", "justify-content",
"justify-items", "justify-self", "left", "letter-spacing", "line-break",
"line-height", "line-height-step", "line-stacking", "line-stacking-ruby",
"line-stacking-shift", "line-stacking-strategy", "list-style",
"list-style-image", "list-style-position", "list-style-type", "margin",
"margin-bottom", "margin-left", "margin-right", "margin-top",
"marker-offset", "marks", "marquee-direction", "marquee-loop",
"marquee-play-count", "marquee-speed", "marquee-style", "max-height",
"max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index",
"nav-left", "nav-right", "nav-up", "object-fit", "object-position",
"opacity", "order", "orphans", "outline",
"outline-color", "outline-offset", "outline-style", "outline-width",
"overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y",
"padding", "padding-bottom", "padding-left", "padding-right", "padding-top",
"page", "page-break-after", "page-break-before", "page-break-inside",
"page-policy", "pause", "pause-after", "pause-before", "perspective",
"perspective-origin", "pitch", "pitch-range", "play-during", "position",
"presentation-level", "punctuation-trim", "quotes", "region-break-after",
"region-break-before", "region-break-inside", "region-fragment",
"rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness",
"right", "rotation", "rotation-point", "ruby-align", "ruby-overhang",
"ruby-position", "ruby-span", "shape-image-threshold", "shape-inside", "shape-margin",
"shape-outside", "size", "speak", "speak-as", "speak-header",
"speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set",
"tab-size", "table-layout", "target", "target-name", "target-new",
"target-position", "text-align", "text-align-last", "text-decoration",
"margin-bottom", "margin-left", "margin-right", "margin-top", "marks",
"marquee-direction", "marquee-loop", "marquee-play-count", "marquee-speed",
"marquee-style", "mask-clip", "mask-composite", "mask-image", "mask-mode",
"mask-origin", "mask-position", "mask-repeat", "mask-size","mask-type",
"max-block-size", "max-height", "max-inline-size",
"max-width", "min-block-size", "min-height", "min-inline-size", "min-width",
"mix-blend-mode", "move-to", "nav-down", "nav-index", "nav-left", "nav-right",
"nav-up", "object-fit", "object-position", "offset", "offset-anchor",
"offset-distance", "offset-path", "offset-position", "offset-rotate",
"opacity", "order", "orphans", "outline", "outline-color", "outline-offset",
"outline-style", "outline-width", "overflow", "overflow-style",
"overflow-wrap", "overflow-x", "overflow-y", "padding", "padding-bottom",
"padding-left", "padding-right", "padding-top", "page", "page-break-after",
"page-break-before", "page-break-inside", "page-policy", "pause",
"pause-after", "pause-before", "perspective", "perspective-origin", "pitch",
"pitch-range", "place-content", "place-items", "place-self", "play-during",
"position", "presentation-level", "punctuation-trim", "quotes",
"region-break-after", "region-break-before", "region-break-inside",
"region-fragment", "rendering-intent", "resize", "rest", "rest-after",
"rest-before", "richness", "right", "rotate", "rotation", "rotation-point",
"row-gap", "ruby-align", "ruby-overhang", "ruby-position", "ruby-span",
"scale", "scroll-behavior", "scroll-margin", "scroll-margin-block",
"scroll-margin-block-end", "scroll-margin-block-start", "scroll-margin-bottom",
"scroll-margin-inline", "scroll-margin-inline-end",
"scroll-margin-inline-start", "scroll-margin-left", "scroll-margin-right",
"scroll-margin-top", "scroll-padding", "scroll-padding-block",
"scroll-padding-block-end", "scroll-padding-block-start",
"scroll-padding-bottom", "scroll-padding-inline", "scroll-padding-inline-end",
"scroll-padding-inline-start", "scroll-padding-left", "scroll-padding-right",
"scroll-padding-top", "scroll-snap-align", "scroll-snap-type",
"shape-image-threshold", "shape-inside", "shape-margin", "shape-outside",
"size", "speak", "speak-as", "speak-header", "speak-numeral",
"speak-punctuation", "speech-rate", "stress", "string-set", "tab-size",
"table-layout", "target", "target-name", "target-new", "target-position",
"text-align", "text-align-last", "text-combine-upright", "text-decoration",
"text-decoration-color", "text-decoration-line", "text-decoration-skip",
"text-decoration-style", "text-emphasis", "text-emphasis-color",
"text-emphasis-position", "text-emphasis-style", "text-height",
"text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow",
"text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position",
"text-wrap", "top", "transform", "transform-origin", "transform-style",
"transition", "transition-delay", "transition-duration",
"transition-property", "transition-timing-function", "unicode-bidi",
"vertical-align", "visibility", "voice-balance", "voice-duration",
"voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress",
"voice-volume", "volume", "white-space", "widows", "width", "word-break",
"word-spacing", "word-wrap", "z-index",
"text-decoration-skip-ink", "text-decoration-style", "text-emphasis",
"text-emphasis-color", "text-emphasis-position", "text-emphasis-style",
"text-height", "text-indent", "text-justify", "text-orientation",
"text-outline", "text-overflow", "text-rendering", "text-shadow",
"text-size-adjust", "text-space-collapse", "text-transform",
"text-underline-position", "text-wrap", "top", "touch-action", "transform", "transform-origin",
"transform-style", "transition", "transition-delay", "transition-duration",
"transition-property", "transition-timing-function", "translate",
"unicode-bidi", "user-select", "vertical-align", "visibility", "voice-balance",
"voice-duration", "voice-family", "voice-pitch", "voice-range", "voice-rate",
"voice-stress", "voice-volume", "volume", "white-space", "widows", "width",
"will-change", "word-break", "word-spacing", "word-wrap", "writing-mode", "z-index",
// SVG-specific
"clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color",
"flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events",
"color-interpolation", "color-interpolation-filters",
"color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering",
"marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke",
"marker", "marker-end", "marker-mid", "marker-start", "paint-order", "shape-rendering", "stroke",
"stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin",
"stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering",
"baseline-shift", "dominant-baseline", "glyph-orientation-horizontal",
"glyph-orientation-vertical", "text-anchor", "writing-mode"
"glyph-orientation-vertical", "text-anchor", "writing-mode","view-transition-name"
], propertyKeywords = keySet(propertyKeywords_);
var nonStandardPropertyKeywords_ = [
"accent-color", "aspect-ratio", "border-block", "border-block-color", "border-block-end",
"border-block-end-color", "border-block-end-style", "border-block-end-width",
"border-block-start", "border-block-start-color", "border-block-start-style",
"border-block-start-width", "border-block-style", "border-block-width",
"border-inline", "border-inline-color", "border-inline-end",
"border-inline-end-color", "border-inline-end-style",
"border-inline-end-width", "border-inline-start", "border-inline-start-color",
"border-inline-start-style", "border-inline-start-width",
"border-inline-style", "border-inline-width", "content-visibility", "margin-block",
"margin-block-end", "margin-block-start", "margin-inline", "margin-inline-end",
"margin-inline-start", "overflow-anchor", "overscroll-behavior", "padding-block", "padding-block-end",
"padding-block-start", "padding-inline", "padding-inline-end",
"padding-inline-start", "scroll-snap-stop", "scrollbar-3d-light-color",
"scrollbar-arrow-color", "scrollbar-base-color", "scrollbar-dark-shadow-color",
"scrollbar-face-color", "scrollbar-highlight-color", "scrollbar-shadow-color",
"scrollbar-3d-light-color", "scrollbar-track-color", "shape-inside",
"searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button",
"searchfield-results-decoration", "zoom"
"scrollbar-track-color", "searchfield-cancel-button", "searchfield-decoration",
"searchfield-results-button", "searchfield-results-decoration", "shape-inside", "zoom"
], nonStandardPropertyKeywords = keySet(nonStandardPropertyKeywords_);
var fontProperties_ = [
"font-family", "src", "unicode-range", "font-variant", "font-feature-settings",
"font-stretch", "font-weight", "font-style"
"font-display", "font-family", "src", "unicode-range", "font-variant",
"font-feature-settings", "font-stretch", "font-weight", "font-style"
], fontProperties = keySet(fontProperties_);
var counterDescriptors_ = [
@@ -561,16 +600,16 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown",
"burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue",
"cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
"darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen",
"darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen",
"darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen",
"darkslateblue", "darkslategray", "darkturquoise", "darkviolet",
"deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick",
"darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet",
"deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick",
"floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite",
"gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew",
"hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
"lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral",
"lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink",
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
"lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink",
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey",
"lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
"maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple",
"mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise",
@@ -580,7 +619,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue",
"purple", "rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown",
"salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue",
"slateblue", "slategray", "snow", "springgreen", "steelblue", "tan",
"slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan",
"teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white",
"whitesmoke", "yellow", "yellowgreen"
], colorKeywords = keySet(colorKeywords_);
@@ -589,23 +628,23 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"above", "absolute", "activeborder", "additive", "activecaption", "afar",
"after-white-space", "ahead", "alias", "all", "all-scroll", "alphabetic", "alternate",
"always", "amharic", "amharic-abegede", "antialiased", "appworkspace",
"arabic-indic", "armenian", "asterisks", "attr", "auto", "avoid", "avoid-column", "avoid-page",
"avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary",
"bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box",
"both", "bottom", "break", "break-all", "break-word", "bullets", "button", "button-bevel",
"arabic-indic", "armenian", "asterisks", "attr", "auto", "auto-flow", "avoid", "avoid-column", "avoid-page",
"avoid-region", "axis-pan", "background", "backwards", "baseline", "below", "bidi-override", "binary",
"bengali", "blink", "block", "block-axis", "blur", "bold", "bolder", "border", "border-box",
"both", "bottom", "break", "break-all", "break-word", "brightness", "bullets", "button",
"buttonface", "buttonhighlight", "buttonshadow", "buttontext", "calc", "cambodian",
"capitalize", "caps-lock-indicator", "caption", "captiontext", "caret",
"cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch",
"cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote",
"col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse",
"compact", "condensed", "contain", "content",
"content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop",
"cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
"decimal-leading-zero", "default", "default-button", "destination-atop",
"compact", "condensed", "conic-gradient", "contain", "content", "contents",
"content-box", "context-menu", "continuous", "contrast", "copy", "counter", "counters", "cover", "crop",
"cross", "crosshair", "cubic-bezier", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal",
"decimal-leading-zero", "default", "default-button", "dense", "destination-atop",
"destination-in", "destination-out", "destination-over", "devanagari", "difference",
"disc", "discard", "disclosure-closed", "disclosure-open", "document",
"dot-dash", "dot-dot-dash",
"dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
"dotted", "double", "down", "drop-shadow", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out",
"element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede",
"ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er",
"ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er",
@@ -614,14 +653,14 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"ethiopic-halehame-sid-et", "ethiopic-halehame-so-et",
"ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig",
"ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed",
"extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
"forwards", "from", "geometricPrecision", "georgian", "graytext", "groove",
"extra-expanded", "fantasy", "fast", "fill", "fill-box", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes",
"forwards", "from", "geometricPrecision", "georgian", "grayscale", "graytext", "grid", "groove",
"gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew",
"help", "hidden", "hide", "higher", "highlight", "highlighttext",
"hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore",
"hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "hue-rotate", "icon", "ignore",
"inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite",
"infobackground", "infotext", "inherit", "initial", "inline", "inline-axis",
"inline-block", "inline-flex", "inline-table", "inset", "inside", "intrinsic", "invert",
"inline-block", "inline-flex", "inline-grid", "inline-table", "inset", "inside", "intrinsic", "invert",
"italic", "japanese-formal", "japanese-informal", "justify", "kannada",
"katakana", "katakana-iroha", "keep-all", "khmer",
"korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal",
@@ -629,41 +668,37 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem",
"local", "logical", "loud", "lower", "lower-alpha", "lower-armenian",
"lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian",
"lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d",
"media-controls-background", "media-current-time-display",
"media-fullscreen-button", "media-mute-button", "media-play-button",
"media-return-to-realtime-button", "media-rewind-button",
"media-seek-back-button", "media-seek-forward-button", "media-slider",
"media-sliderthumb", "media-time-remaining-display", "media-volume-slider",
"media-volume-slider-container", "media-volume-sliderthumb", "medium",
"menu", "menulist", "menulist-button", "menulist-text",
"menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic",
"mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize",
"lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "manipulation", "match", "matrix", "matrix3d",
"media-play-button", "media-slider", "media-sliderthumb",
"media-volume-slider", "media-volume-sliderthumb", "medium",
"menu", "menulist", "menulist-button",
"menutext", "message-box", "middle", "min-intrinsic",
"mix", "mongolian", "monospace", "move", "multiple", "multiple_mask_images", "multiply", "myanmar", "n-resize",
"narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop",
"no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap",
"ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote",
"ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "opacity", "open-quote",
"optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset",
"outside", "outside-shape", "overlay", "overline", "padding", "padding-box",
"painted", "page", "paused", "persian", "perspective", "plus-darker", "plus-lighter",
"painted", "page", "paused", "persian", "perspective", "pinch-zoom", "plus-darker", "plus-lighter",
"pointer", "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d",
"progress", "push-button", "radial-gradient", "radio", "read-only",
"read-write", "read-write-plaintext-only", "rectangle", "region",
"relative", "repeat", "repeating-linear-gradient",
"repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse",
"relative", "repeat", "repeating-linear-gradient", "repeating-radial-gradient",
"repeating-conic-gradient", "repeat-x", "repeat-y", "reset", "reverse",
"rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY",
"rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running",
"s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
"scroll", "scrollbar", "se-resize", "searchfield",
"s-resize", "sans-serif", "saturate", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen",
"scroll", "scrollbar", "scroll-position", "se-resize", "searchfield",
"searchfield-cancel-button", "searchfield-decoration",
"searchfield-results-button", "searchfield-results-decoration",
"semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama",
"searchfield-results-button", "searchfield-results-decoration", "self-start", "self-end",
"semi-condensed", "semi-expanded", "separate", "sepia", "serif", "show", "sidama",
"simp-chinese-formal", "simp-chinese-informal", "single",
"skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal",
"slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow",
"small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
"source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square",
"square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
"subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table",
"source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "space-evenly", "spell-out", "square",
"square-button", "start", "static", "status-bar", "stretch", "stroke", "stroke-box", "sub",
"subpixel-antialiased", "svg_masks", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table",
"table-caption", "table-cell", "table-column", "table-column-group",
"table-footer-group", "table-header-group", "table-row", "table-row-group",
"tamil",
@@ -671,12 +706,12 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
"thick", "thin", "threeddarkshadow", "threedface", "threedhighlight",
"threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er",
"tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top",
"trad-chinese-formal", "trad-chinese-informal",
"trad-chinese-formal", "trad-chinese-informal", "transform",
"translate", "translate3d", "translateX", "translateY", "translateZ",
"transparent", "ultra-condensed", "ultra-expanded", "underline", "up",
"transparent", "ultra-condensed", "ultra-expanded", "underline", "unidirectional-pan", "unset", "up",
"upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal",
"upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url",
"var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted",
"var", "vertical", "vertical-text", "view-box", "visible", "visibleFill", "visiblePainted",
"visibleStroke", "visual", "w-resize", "wait", "wave", "wider",
"window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor",
"xx-large", "xx-small"
@@ -730,6 +765,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
lineComment: "//",
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
@@ -743,8 +779,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
}
},
":": function(stream) {
if (stream.match(/\s*\{/))
return [null, "{"];
if (stream.match(/^\s*\{/, false))
return [null, null]
return false;
},
"$": function(stream) {
@@ -772,6 +808,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
valueKeywords: valueKeywords,
fontProperties: fontProperties,
allowNested: true,
lineComment: "//",
tokenHooks: {
"/": function(stream, state) {
if (stream.eat("/")) {
@@ -786,7 +823,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
},
"@": function(stream) {
if (stream.eat("{")) return [null, "interpolation"];
if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/, false)) return false;
if (stream.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/i, false)) return false;
stream.eatWhile(/[\w\\\-]/);
if (stream.match(/^\s*:/, false))
return ["variable-2", "variable-definition"];

View File

@@ -1,103 +0,0 @@
<!doctype html>
<title>CodeMirror: Closure Stylesheets (GSS) mode</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../../doc/docs.css">
<link rel="stylesheet" href="../../lib/codemirror.css">
<link rel="stylesheet" href="../../addon/hint/show-hint.css">
<script src="../../lib/codemirror.js"></script>
<script src="css.js"></script>
<script src="../../addon/hint/show-hint.js"></script>
<script src="../../addon/hint/css-hint.js"></script>
<style>.CodeMirror {background: #f8f8f8;}</style>
<div id=nav>
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
<ul>
<li><a href="../../index.html">Home</a>
<li><a href="../../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a href="../index.html">Language modes</a>
<li><a class=active href="#">Closure Stylesheets (GSS)</a>
</ul>
</div>
<article>
<h2>Closure Stylesheets (GSS) mode</h2>
<form><textarea id="code" name="code">
/* Some example Closure Stylesheets */
@provide 'some.styles';
@require 'other.styles';
@component {
@def FONT_FAMILY "Times New Roman", Georgia, Serif;
@def FONT_SIZE_NORMAL 15px;
@def FONT_NORMAL normal FONT_SIZE_NORMAL FONT_FAMILY;
@def BG_COLOR rgb(235, 239, 249);
@def DIALOG_BORDER_COLOR rgb(107, 144, 218);
@def DIALOG_BG_COLOR BG_COLOR;
@def LEFT_HAND_NAV_WIDTH 180px;
@def LEFT_HAND_NAV_PADDING 3px;
@defmixin size(WIDTH, HEIGHT) {
width: WIDTH;
height: HEIGHT;
}
body {
background-color: BG_COLOR;
margin: 0;
padding: 3em 6em;
font: FONT_NORMAL;
color: #000;
}
#navigation a {
font-weight: bold;
text-decoration: none !important;
}
.dialog {
background-color: DIALOG_BG_COLOR;
border: 1px solid DIALOG_BORDER_COLOR;
}
.content {
position: absolute;
margin-left: add(LEFT_HAND_NAV_PADDING, /* padding left */
LEFT_HAND_NAV_WIDTH,
LEFT_HAND_NAV_PADDING); /* padding right */
}
.logo {
@mixin size(150px, 55px);
background-image: url('http://www.google.com/images/logo_sm.gif');
}
}
</textarea></form>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
extraKeys: {"Ctrl-Space": "autocomplete"},
lineNumbers: true,
matchBrackets: "text/x-less",
mode: "text/x-gss"
});
</script>
<p>A mode for <a href="https://github.com/google/closure-stylesheets">Closure Stylesheets</a> (GSS).</p>
<p><strong>MIME type defined:</strong> <code>text/x-gss</code>.</p>
<p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#gss_*">normal</a>, <a href="../../test/index.html#verbose,gss_*">verbose</a>.</p>
</article>

View File

@@ -1,17 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function() {
"use strict";
var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-gss");
function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "gss"); }
MT("atComponent",
"[def @component] {",
"[tag foo] {",
" [property color]: [keyword black];",
"}",
"}");
})();

View File

@@ -1,152 +0,0 @@
<!doctype html>
<title>CodeMirror: LESS mode</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../../doc/docs.css">
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="../../addon/edit/matchbrackets.js"></script>
<script src="css.js"></script>
<style>.CodeMirror {border: 1px solid #ddd; line-height: 1.2;}</style>
<div id=nav>
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
<ul>
<li><a href="../../index.html">Home</a>
<li><a href="../../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a href="../index.html">Language modes</a>
<li><a class=active href="#">LESS</a>
</ul>
</div>
<article>
<h2>LESS mode</h2>
<form><textarea id="code" name="code">@media screen and (device-aspect-ratio: 16/9) { … }
@media screen and (device-aspect-ratio: 1280/720) { … }
@media screen and (device-aspect-ratio: 2560/1440) { … }
html:lang(fr-be)
tr:nth-child(2n+1) /* represents every odd row of an HTML table */
img:nth-of-type(2n+1) { float: right; }
img:nth-of-type(2n) { float: left; }
body > h2:not(:first-of-type):not(:last-of-type)
html|*:not(:link):not(:visited)
*|*:not(:hover)
p::first-line { text-transform: uppercase }
@namespace foo url(http://www.example.com);
foo|h1 { color: blue } /* first rule */
span[hello="Ocean"][goodbye="Land"]
E[foo]{
padding:65px;
}
input[type="search"]::-webkit-search-decoration,
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5
}
button::-moz-focus-inner,
input::-moz-focus-inner { // Inner padding and border oddities in FF3/4
padding: 0;
border: 0;
}
.btn {
// reset here as of 2.0.3 due to Recess property order
border-color: #ccc;
border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) rgba(0,0,0,.25);
}
fieldset span button, fieldset span input[type="file"] {
font-size:12px;
font-family:Arial, Helvetica, sans-serif;
}
.rounded-corners (@radius: 5px) {
border-radius: @radius;
-webkit-border-radius: @radius;
-moz-border-radius: @radius;
}
@import url("something.css");
@light-blue: hsl(190, 50%, 65%);
#menu {
position: absolute;
width: 100%;
z-index: 3;
clear: both;
display: block;
background-color: @blue;
height: 42px;
border-top: 2px solid lighten(@alpha-blue, 20%);
border-bottom: 2px solid darken(@alpha-blue, 25%);
.box-shadow(0, 1px, 8px, 0.6);
-moz-box-shadow: 0 0 0 #000; // Because firefox sucks.
&.docked {
background-color: hsla(210, 60%, 40%, 0.4);
}
&:hover {
background-color: @blue;
}
#dropdown {
margin: 0 0 0 117px;
padding: 0;
padding-top: 5px;
display: none;
width: 190px;
border-top: 2px solid @medium;
color: @highlight;
border: 2px solid darken(@medium, 25%);
border-left-color: darken(@medium, 15%);
border-right-color: darken(@medium, 15%);
border-top-width: 0;
background-color: darken(@medium, 10%);
ul {
padding: 0px;
}
li {
font-size: 14px;
display: block;
text-align: left;
padding: 0;
border: 0;
a {
display: block;
padding: 0px 15px;
text-decoration: none;
color: white;
&:hover {
background-color: darken(@medium, 15%);
text-decoration: none;
}
}
}
.border-radius(5px, bottom);
.box-shadow(0, 6px, 8px, 0.5);
}
}
</textarea></form>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
lineNumbers : true,
matchBrackets : true,
mode: "text/x-less"
});
</script>
<p>The LESS mode is a sub-mode of the <a href="index.html">CSS mode</a> (defined in <code>css.js</code>).</p>
<p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#less_*">normal</a>, <a href="../../test/index.html#verbose,less_*">verbose</a>.</p>
</article>

View File

@@ -1,157 +0,0 @@
<!doctype html>
<title>CodeMirror: SCSS mode</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../../doc/docs.css">
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="css.js"></script>
<style>.CodeMirror {background: #f8f8f8;}</style>
<div id=nav>
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
<ul>
<li><a href="../../index.html">Home</a>
<li><a href="../../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a href="../index.html">Language modes</a>
<li><a class=active href="#">SCSS</a>
</ul>
</div>
<article>
<h2>SCSS mode</h2>
<form><textarea id="code" name="code">
/* Some example SCSS */
@import "compass/css3";
$variable: #333;
$blue: #3bbfce;
$margin: 16px;
.content-navigation {
#nested {
background-color: black;
}
border-color: $blue;
color:
darken($blue, 9%);
}
.border {
padding: $margin / 2;
margin: $margin / 2;
border-color: $blue;
}
@mixin table-base {
th {
text-align: center;
font-weight: bold;
}
td, th {padding: 2px}
}
table.hl {
margin: 2em 0;
td.ln {
text-align: right;
}
}
li {
font: {
family: serif;
weight: bold;
size: 1.2em;
}
}
@mixin left($dist) {
float: left;
margin-left: $dist;
}
#data {
@include left(10px);
@include table-base;
}
.source {
@include flow-into(target);
border: 10px solid green;
margin: 20px;
width: 200px; }
.new-container {
@include flow-from(target);
border: 10px solid red;
margin: 20px;
width: 200px; }
body {
margin: 0;
padding: 3em 6em;
font-family: tahoma, arial, sans-serif;
color: #000;
}
@mixin yellow() {
background: yellow;
}
.big {
font-size: 14px;
}
.nested {
@include border-radius(3px);
@extend .big;
p {
background: whitesmoke;
a {
color: red;
}
}
}
#navigation a {
font-weight: bold;
text-decoration: none !important;
}
h1 {
font-size: 2.5em;
}
h2 {
font-size: 1.7em;
}
h1:before, h2:before {
content: "::";
}
code {
font-family: courier, monospace;
font-size: 80%;
color: #418A8A;
}
</textarea></form>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
lineNumbers: true,
matchBrackets: true,
mode: "text/x-scss"
});
</script>
<p>The SCSS mode is a sub-mode of the <a href="index.html">CSS mode</a> (defined in <code>css.js</code>).</p>
<p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#scss_*">normal</a>, <a href="../../test/index.html#verbose,scss_*">verbose</a>.</p>
</article>

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -72,7 +72,7 @@
}
}
// donot handle --> as valid ruby, make it HTML close comment instead
// do not handle --> as valid ruby, make it HTML close comment instead
if (state.startOfLine && !stream.match("-->", false) && (ch == "=" || ch == "-" )) {
state.tokenize = ruby;
return state.tokenize(stream, state);
@@ -98,8 +98,8 @@
return {
// default to html mode
startState: function() {
var htmlState = htmlMode.startState();
var rubyState = rubyMode.startState();
var htmlState = CodeMirror.startState(htmlMode);
var rubyState = CodeMirror.startState(rubyMode);
return {
htmlState: htmlState,
rubyState: rubyState,

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -14,7 +14,16 @@
"use strict";
CodeMirror.defineMode("htmlembedded", function(config, parserConfig) {
var closeComment = parserConfig.closeComment || "--%>"
return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "htmlmixed"), {
open: parserConfig.openComment || "<%--",
close: closeComment,
delimStyle: "comment",
mode: {token: function(stream) {
stream.skipTo(closeComment) || stream.skipToEnd()
return "comment"
}}
}, {
open: parserConfig.open || parserConfig.scriptStartRegex || "<%",
close: parserConfig.close || parserConfig.scriptEndRegex || "%>",
mode: CodeMirror.getMode(config, parserConfig.scriptingModeSpec)

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -14,7 +14,7 @@
var defaultTags = {
script: [
["lang", /(javascript|babel)/i, "javascript"],
["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"],
["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i, "javascript"],
["type", /./, "text/plain"],
[null, null, "javascript"]
],
@@ -46,11 +46,11 @@
function getAttrValue(text, attr) {
var match = text.match(getAttrRegexp(attr))
return match ? match[2] : ""
return match ? /^\s*(.*?)\s*$/.exec(match[2])[1] : ""
}
function getTagRegexp(tagName, anchored) {
return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i");
return new RegExp((anchored ? "^" : "") + "<\/\\s*" + tagName + "\\s*>", "i");
}
function addTags(from, to) {
@@ -74,7 +74,8 @@
name: "xml",
htmlMode: true,
multilineTagIndentFactor: parserConfig.multilineTagIndentFactor,
multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag
multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag,
allowMissingTagName: parserConfig.allowMissingTagName,
});
var tags = {};
@@ -105,7 +106,7 @@
return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState));
};
state.localMode = mode;
state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, ""));
state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, "", ""));
} else if (state.inTag) {
state.inTag += stream.current()
if (stream.eol()) state.inTag += " "
@@ -115,7 +116,7 @@
return {
startState: function () {
var state = htmlMode.startState();
var state = CodeMirror.startState(htmlMode);
return {token: html, inTag: null, localMode: null, localState: null, htmlState: state};
},
@@ -133,11 +134,11 @@
return state.token(stream, state);
},
indent: function (state, textAfter) {
indent: function (state, textAfter, line) {
if (!state.localMode || /^\s*<\//.test(textAfter))
return htmlMode.indent(state.htmlState, textAfter);
return htmlMode.indent(state.htmlState, textAfter, line);
else if (state.localMode.indent)
return state.localMode.indent(state.localState, textAfter);
return state.localMode.indent(state.localState, textAfter, line);
else
return CodeMirror.Pass;
},

View File

@@ -1,7 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// TODO actually recognize syntax of TypeScript constructs
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -13,16 +11,12 @@
})(function(CodeMirror) {
"use strict";
function expressionAllowed(stream, state, backUp) {
return /^(?:operator|sof|keyword c|case|new|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
}
CodeMirror.defineMode("javascript", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var statementIndent = parserConfig.statementIndent;
var jsonldMode = parserConfig.jsonld;
var jsonMode = parserConfig.json || jsonldMode;
var trackScope = parserConfig.trackScope !== false
var isTS = parserConfig.typescript;
var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
@@ -30,54 +24,24 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var keywords = function(){
function kw(type) {return {type: type, style: "keyword"};}
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
var jsKeywords = {
return {
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
"return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C,
"var": kw("var"), "const": kw("var"), "let": kw("var"),
"return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
"debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
"function": kw("function"), "catch": kw("catch"),
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
"in": operator, "typeof": operator, "instanceof": operator,
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
"this": kw("this"), "class": kw("class"), "super": kw("atom"),
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
"await": C
};
// Extend the 'normal' keywords with the TypeScript language extensions
if (isTS) {
var type = {type: "variable", style: "variable-3"};
var tsKeywords = {
// object-like things
"interface": kw("class"),
"implements": C,
"namespace": C,
"module": kw("module"),
"enum": kw("module"),
// scope modifiers
"public": kw("modifier"),
"private": kw("modifier"),
"protected": kw("modifier"),
"abstract": kw("modifier"),
// operators
"as": operator,
// types
"string": type, "number": type, "boolean": type, "any": type
};
for (var attr in tsKeywords) {
jsKeywords[attr] = tsKeywords[attr];
}
}
return jsKeywords;
}();
var isOperatorChar = /[+\-*&%=<>!?|~^]/;
var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
function readRegexp(stream) {
@@ -104,7 +68,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (ch == '"' || ch == "'") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
} else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) {
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
return ret("number", "number");
} else if (ch == "." && stream.match("..")) {
return ret("spread", "meta");
@@ -112,17 +76,10 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
return ret(ch);
} else if (ch == "=" && stream.eat(">")) {
return ret("=>", "operator");
} else if (ch == "0" && stream.eat(/x/i)) {
stream.eatWhile(/[\da-f]/i);
return ret("number", "number");
} else if (ch == "0" && stream.eat(/o/i)) {
stream.eatWhile(/[0-7]/i);
return ret("number", "number");
} else if (ch == "0" && stream.eat(/b/i)) {
stream.eatWhile(/[01]/i);
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
return ret("number", "number");
} else if (/\d/.test(ch)) {
stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
return ret("number", "number");
} else if (ch == "/") {
if (stream.eat("*")) {
@@ -133,26 +90,47 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
return ret("comment", "comment");
} else if (expressionAllowed(stream, state, 1)) {
readRegexp(stream);
stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/);
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
return ret("regexp", "string-2");
} else {
stream.eatWhile(isOperatorChar);
stream.eat("=");
return ret("operator", "operator", stream.current());
}
} else if (ch == "`") {
state.tokenize = tokenQuasi;
return tokenQuasi(stream, state);
} else if (ch == "#") {
} else if (ch == "#" && stream.peek() == "!") {
stream.skipToEnd();
return ret("error", "error");
return ret("meta", "meta");
} else if (ch == "#" && stream.eatWhile(wordRE)) {
return ret("variable", "property")
} else if (ch == "<" && stream.match("!--") ||
(ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) {
stream.skipToEnd()
return ret("comment", "comment")
} else if (isOperatorChar.test(ch)) {
stream.eatWhile(isOperatorChar);
if (ch != ">" || !state.lexical || state.lexical.type != ">") {
if (stream.eat("=")) {
if (ch == "!" || ch == "=") stream.eat("=")
} else if (/[<>*+\-|&?]/.test(ch)) {
stream.eat(ch)
if (ch == ">") stream.eat(ch)
}
}
if (ch == "?" && stream.eat(".")) return ret(".")
return ret("operator", "operator", stream.current());
} else if (wordRE.test(ch)) {
stream.eatWhile(wordRE);
var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
ret("variable", "variable", word);
var word = stream.current()
if (state.lastType != ".") {
if (keywords.propertyIsEnumerable(word)) {
var kw = keywords[word]
return ret(kw.type, kw.style, word)
}
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
return ret("async", "keyword", word)
}
return ret("variable", "variable", word)
}
}
@@ -209,19 +187,28 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var arrow = stream.string.indexOf("=>", stream.start);
if (arrow < 0) return;
if (isTS) { // Try to skip TypeScript return type declarations after the arguments
var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
if (m) arrow = m.index
}
var depth = 0, sawSomething = false;
for (var pos = arrow - 1; pos >= 0; --pos) {
var ch = stream.string.charAt(pos);
var bracket = brackets.indexOf(ch);
if (bracket >= 0 && bracket < 3) {
if (!depth) { ++pos; break; }
if (--depth == 0) break;
if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
} else if (bracket >= 3 && bracket < 6) {
++depth;
} else if (wordRE.test(ch)) {
sawSomething = true;
} else if (/["'\/]/.test(ch)) {
return;
} else if (/["'\/`]/.test(ch)) {
for (;; --pos) {
if (pos == 0) return
var next = stream.string.charAt(pos - 1)
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
}
} else if (sawSomething && !depth) {
++pos;
break;
@@ -232,7 +219,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
// Parser
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true};
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true,
"regexp": true, "this": true, "import": true, "jsonld-keyword": true};
function JSLexical(indented, column, type, align, prev, info) {
this.indented = indented;
@@ -244,6 +232,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
}
function inScope(state, varname) {
if (!trackScope) return false
for (var v = state.localVars; v; v = v.next)
if (v.name == varname) return true;
for (var cx = state.context; cx; cx = cx.prev) {
@@ -283,35 +272,70 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
pass.apply(null, arguments);
return true;
}
function inList(name, list) {
for (var v = list; v; v = v.next) if (v.name == name) return true
return false;
}
function register(varname) {
function inList(list) {
for (var v = list; v; v = v.next)
if (v.name == varname) return true;
return false;
}
var state = cx.state;
cx.marked = "def";
if (!trackScope) return
if (state.context) {
if (inList(state.localVars)) return;
state.localVars = {name: varname, next: state.localVars};
} else {
if (inList(state.globalVars)) return;
if (parserConfig.globalVars)
state.globalVars = {name: varname, next: state.globalVars};
if (state.lexical.info == "var" && state.context && state.context.block) {
// FIXME function decls are also not block scoped
var newContext = registerVarScoped(varname, state.context)
if (newContext != null) {
state.context = newContext
return
}
} else if (!inList(varname, state.localVars)) {
state.localVars = new Var(varname, state.localVars)
return
}
}
// Fall through means this is global
if (parserConfig.globalVars && !inList(varname, state.globalVars))
state.globalVars = new Var(varname, state.globalVars)
}
function registerVarScoped(varname, context) {
if (!context) {
return null
} else if (context.block) {
var inner = registerVarScoped(varname, context.prev)
if (!inner) return null
if (inner == context.prev) return context
return new Context(inner, context.vars, true)
} else if (inList(varname, context.vars)) {
return context
} else {
return new Context(context.prev, new Var(varname, context.vars), false)
}
}
function isModifier(name) {
return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
}
// Combinators
var defaultVars = {name: "this", next: {name: "arguments"}};
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
function Var(name, next) { this.name = name; this.next = next }
var defaultVars = new Var("this", new Var("arguments", null))
function pushcontext() {
cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
cx.state.localVars = defaultVars;
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
cx.state.localVars = defaultVars
}
function pushblockcontext() {
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
cx.state.localVars = null
}
pushcontext.lex = pushblockcontext.lex = true
function popcontext() {
cx.state.localVars = cx.state.context.vars;
cx.state.context = cx.state.context.prev;
cx.state.localVars = cx.state.context.vars
cx.state.context = cx.state.context.prev
}
popcontext.lex = true
function pushlex(type, info) {
var result = function() {
var state = cx.state, indent = state.indented;
@@ -336,56 +360,87 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function expect(wanted) {
function exp(type) {
if (type == wanted) return cont();
else if (wanted == ";") return pass();
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
else return cont(exp);
};
return exp;
}
function statement(type, value) {
if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex);
if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
if (type == "{") return cont(pushlex("}"), block, poplex);
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
if (type == "debugger") return cont(expect(";"));
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
if (type == ";") return cont();
if (type == "if") {
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
cx.state.cc.pop()();
return cont(pushlex("form"), expression, statement, poplex, maybeelse);
return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
}
if (type == "function") return cont(functiondef);
if (type == "for") return cont(pushlex("form"), forspec, statement, poplex);
if (type == "variable") return cont(pushlex("stat"), maybelabel);
if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
block, poplex, poplex);
if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex);
if (type == "class" || (isTS && value == "interface")) {
cx.marked = "keyword"
return cont(pushlex("form", type == "class" ? type : value), className, poplex)
}
if (type == "variable") {
if (isTS && value == "declare") {
cx.marked = "keyword"
return cont(statement)
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
cx.marked = "keyword"
if (value == "enum") return cont(enumdef);
else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";"));
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
} else if (isTS && value == "namespace") {
cx.marked = "keyword"
return cont(pushlex("form"), expression, statement, poplex)
} else if (isTS && value == "abstract") {
cx.marked = "keyword"
return cont(statement)
} else {
return cont(pushlex("stat"), maybelabel);
}
}
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
block, poplex, poplex, popcontext);
if (type == "case") return cont(expression, expect(":"));
if (type == "default") return cont(expect(":"));
if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
statement, poplex, popcontext);
if (type == "class") return cont(pushlex("form"), className, poplex);
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex)
if (type == "async") return cont(statement)
if (value == "@") return cont(expression, statement)
return pass(pushlex("stat"), expression, expect(";"), poplex);
}
function expression(type) {
return expressionInner(type, false);
function maybeCatchBinding(type) {
if (type == "(") return cont(funarg, expect(")"))
}
function expressionNoComma(type) {
return expressionInner(type, true);
function expression(type, value) {
return expressionInner(type, value, false);
}
function expressionInner(type, noComma) {
function expressionNoComma(type, value) {
return expressionInner(type, value, true);
}
function parenExpr(type) {
if (type != "(") return pass()
return cont(pushlex(")"), maybeexpression, expect(")"), poplex)
}
function expressionInner(type, value, noComma) {
if (cx.state.fatArrowAt == cx.stream.start) {
var body = noComma ? arrowBodyNoComma : arrowBody;
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext);
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
}
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
if (type == "function") return cont(functiondef, maybeop);
if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression);
if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop);
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
@@ -397,13 +452,9 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expression);
}
function maybeexpressionNoComma(type) {
if (type.match(/[;\}\)\],]/)) return pass();
return pass(expressionNoComma);
}
function maybeoperatorComma(type, value) {
if (type == ",") return cont(expression);
if (type == ",") return cont(maybeexpression);
return maybeoperatorNoComma(type, value, false);
}
function maybeoperatorNoComma(type, value, noComma) {
@@ -411,7 +462,9 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
var expr = noComma == false ? expression : expressionNoComma;
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
if (type == "operator") {
if (/\+\+|--/.test(value)) return cont(me);
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false))
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
if (value == "?") return cont(expression, expect(":"), expr);
return cont(expr);
}
@@ -420,11 +473,17 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
if (type == ".") return cont(property, me);
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
if (type == "regexp") {
cx.state.lastType = cx.marked = "operator"
cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
return cont(expr)
}
}
function quasi(type, value) {
if (type != "quasi") return pass();
if (value.slice(value.length - 2) != "${") return cont(quasi);
return cont(expression, continueQuasi);
return cont(maybeexpression, continueQuasi);
}
function continueQuasi(type) {
if (type == "}") {
@@ -444,6 +503,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function maybeTarget(noComma) {
return function(type) {
if (type == ".") return cont(noComma ? targetNoComma : target);
else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
else return pass(noComma ? expressionNoComma : expression);
};
}
@@ -461,21 +521,33 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "variable") {cx.marked = "property"; return cont();}
}
function objprop(type, value) {
if (type == "variable" || cx.style == "keyword") {
if (type == "async") {
cx.marked = "property";
return cont(objprop);
} else if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
if (value == "get" || value == "set") return cont(getterSetter);
var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
cx.state.fatArrowAt = cx.stream.pos + m[0].length
return cont(afterprop);
} else if (type == "number" || type == "string") {
cx.marked = jsonldMode ? "property" : (cx.style + " property");
return cont(afterprop);
} else if (type == "jsonld-keyword") {
return cont(afterprop);
} else if (type == "modifier") {
} else if (isTS && isModifier(value)) {
cx.marked = "keyword"
return cont(objprop)
} else if (type == "[") {
return cont(expression, expect("]"), afterprop);
return cont(expression, maybetype, expect("]"), afterprop);
} else if (type == "spread") {
return cont(expression);
return cont(expressionNoComma, afterprop);
} else if (value == "*") {
cx.marked = "keyword";
return cont(objprop);
} else if (type == ":") {
return pass(afterprop)
}
}
function getterSetter(type) {
@@ -487,18 +559,22 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == ":") return cont(expressionNoComma);
if (type == "(") return pass(functiondef);
}
function commasep(what, end) {
function proceed(type) {
if (type == ",") {
function commasep(what, end, sep) {
function proceed(type, value) {
if (sep ? sep.indexOf(type) > -1 : type == ",") {
var lex = cx.state.lexical;
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
return cont(what, proceed);
return cont(function(type, value) {
if (type == end || value == end) return pass()
return pass(what)
}, proceed);
}
if (type == end) return cont();
if (type == end || value == end) return cont();
if (sep && sep.indexOf(";") > -1) return pass(what)
return cont(expect(end));
}
return function(type) {
if (type == end) return cont();
return function(type, value) {
if (type == end || value == end) return cont();
return pass(what, proceed);
};
}
@@ -511,23 +587,111 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "}") return cont();
return pass(statement, block);
}
function maybetype(type) {
if (isTS && type == ":") return cont(typedef);
function maybetype(type, value) {
if (isTS) {
if (type == ":") return cont(typeexpr);
if (value == "?") return cont(maybetype);
}
}
function maybedefault(_, value) {
if (value == "=") return cont(expressionNoComma);
function maybetypeOrIn(type, value) {
if (isTS && (type == ":" || value == "in")) return cont(typeexpr)
}
function typedef(type) {
if (type == "variable") {cx.marked = "variable-3"; return cont();}
function mayberettype(type) {
if (isTS && type == ":") {
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
else return cont(typeexpr)
}
}
function vardef() {
function isKW(_, value) {
if (value == "is") {
cx.marked = "keyword"
return cont()
}
}
function typeexpr(type, value) {
if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") {
cx.marked = "keyword"
return cont(value == "typeof" ? expressionNoComma : typeexpr)
}
if (type == "variable" || value == "void") {
cx.marked = "type"
return cont(afterType)
}
if (value == "|" || value == "&") return cont(typeexpr)
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType)
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType)
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
if (type == "quasi") { return pass(quasiType, afterType); }
}
function maybeReturnType(type) {
if (type == "=>") return cont(typeexpr)
}
function typeprops(type) {
if (type.match(/[\}\)\]]/)) return cont()
if (type == "," || type == ";") return cont(typeprops)
return pass(typeprop, typeprops)
}
function typeprop(type, value) {
if (type == "variable" || cx.style == "keyword") {
cx.marked = "property"
return cont(typeprop)
} else if (value == "?" || type == "number" || type == "string") {
return cont(typeprop)
} else if (type == ":") {
return cont(typeexpr)
} else if (type == "[") {
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
} else if (type == "(") {
return pass(functiondecl, typeprop)
} else if (!type.match(/[;\}\)\],]/)) {
return cont()
}
}
function quasiType(type, value) {
if (type != "quasi") return pass();
if (value.slice(value.length - 2) != "${") return cont(quasiType);
return cont(typeexpr, continueQuasiType);
}
function continueQuasiType(type) {
if (type == "}") {
cx.marked = "string-2";
cx.state.tokenize = tokenQuasi;
return cont(quasiType);
}
}
function typearg(type, value) {
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
if (type == ":") return cont(typeexpr)
if (type == "spread") return cont(typearg)
return pass(typeexpr)
}
function afterType(type, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
if (type == "[") return cont(typeexpr, expect("]"), afterType)
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
if (value == "?") return cont(typeexpr, expect(":"), typeexpr)
}
function maybeTypeArgs(_, value) {
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
}
function typeparam() {
return pass(typeexpr, maybeTypeDefault)
}
function maybeTypeDefault(_, value) {
if (value == "=") return cont(typeexpr)
}
function vardef(_, value) {
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
return pass(pattern, maybetype, maybeAssign, vardefCont);
}
function pattern(type, value) {
if (type == "modifier") return cont(pattern)
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
if (type == "variable") { register(value); return cont(); }
if (type == "spread") return cont(pattern);
if (type == "[") return contCommasep(pattern, "]");
if (type == "[") return contCommasep(eltpattern, "]");
if (type == "{") return contCommasep(proppattern, "}");
}
function proppattern(type, value) {
@@ -538,8 +702,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "variable") cx.marked = "property";
if (type == "spread") return cont(pattern);
if (type == "}") return pass();
if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern);
return cont(expect(":"), pattern, maybeAssign);
}
function eltpattern() {
return pass(pattern, maybeAssign)
}
function maybeAssign(_type, value) {
if (value == "=") return cont(expressionNoComma);
}
@@ -549,73 +717,111 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
function maybeelse(type, value) {
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
}
function forspec(type) {
if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex);
function forspec(type, value) {
if (value == "await") return cont(forspec);
if (type == "(") return cont(pushlex(")"), forspec1, poplex);
}
function forspec1(type) {
if (type == "var") return cont(vardef, expect(";"), forspec2);
if (type == ";") return cont(forspec2);
if (type == "variable") return cont(formaybeinof);
return pass(expression, expect(";"), forspec2);
}
function formaybeinof(_type, value) {
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
return cont(maybeoperatorComma, forspec2);
if (type == "var") return cont(vardef, forspec2);
if (type == "variable") return cont(forspec2);
return pass(forspec2)
}
function forspec2(type, value) {
if (type == ";") return cont(forspec3);
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); }
return pass(expression, expect(";"), forspec3);
}
function forspec3(type) {
if (type != ")") cont(expression);
if (type == ")") return cont()
if (type == ";") return cont(forspec2)
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) }
return pass(expression, forspec2)
}
function functiondef(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
if (type == "variable") {register(value); return cont(functiondef);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext);
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
}
function funarg(type) {
function functiondecl(type, value) {
if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);}
if (type == "variable") {register(value); return cont(functiondecl);}
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext);
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl)
}
function typename(type, value) {
if (type == "keyword" || type == "variable") {
cx.marked = "type"
return cont(typename)
} else if (value == "<") {
return cont(pushlex(">"), commasep(typeparam, ">"), poplex)
}
}
function funarg(type, value) {
if (value == "@") cont(expression, funarg)
if (type == "spread") return cont(funarg);
return pass(pattern, maybetype, maybedefault);
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
if (isTS && type == "this") return cont(maybetype, maybeAssign)
return pass(pattern, maybetype, maybeAssign);
}
function classExpression(type, value) {
// Class expressions may have an optional name.
if (type == "variable") return className(type, value);
return classNameAfter(type, value);
}
function className(type, value) {
if (type == "variable") {register(value); return cont(classNameAfter);}
}
function classNameAfter(type, value) {
if (value == "extends") return cont(expression, classNameAfter);
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
if (value == "implements") cx.marked = "keyword";
return cont(isTS ? typeexpr : expression, classNameAfter);
}
if (type == "{") return cont(pushlex("}"), classBody, poplex);
}
function classBody(type, value) {
if (type == "variable" || cx.style == "keyword") {
if (value == "static") {
cx.marked = "keyword";
return cont(classBody);
}
cx.marked = "property";
if (value == "get" || value == "set") return cont(classGetterSetter, functiondef, classBody);
return cont(functiondef, classBody);
if (type == "async" ||
(type == "variable" &&
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) {
cx.marked = "keyword";
return cont(classBody);
}
if (type == "variable" || cx.style == "keyword") {
cx.marked = "property";
return cont(classfield, classBody);
}
if (type == "number" || type == "string") return cont(classfield, classBody);
if (type == "[")
return cont(expression, maybetype, expect("]"), classfield, classBody)
if (value == "*") {
cx.marked = "keyword";
return cont(classBody);
}
if (type == ";") return cont(classBody);
if (isTS && type == "(") return pass(functiondecl, classBody)
if (type == ";" || type == ",") return cont(classBody);
if (type == "}") return cont();
if (value == "@") return cont(expression, classBody)
}
function classGetterSetter(type) {
if (type != "variable") return pass();
cx.marked = "property";
return cont();
function classfield(type, value) {
if (value == "!") return cont(classfield)
if (value == "?") return cont(classfield)
if (type == ":") return cont(typeexpr, maybeAssign)
if (value == "=") return cont(expressionNoComma)
var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"
return pass(isInterface ? functiondecl : functiondef)
}
function afterExport(_type, value) {
function afterExport(type, value) {
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
return pass(statement);
}
function exportField(type, value) {
if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
if (type == "variable") return pass(expressionNoComma, exportField);
}
function afterImport(type) {
if (type == "string") return cont();
return pass(importSpec, maybeFrom);
if (type == "(") return pass(expression);
if (type == ".") return pass(maybeoperatorComma);
return pass(importSpec, maybeMoreImports, maybeFrom);
}
function importSpec(type, value) {
if (type == "{") return contCommasep(importSpec, "}");
@@ -623,6 +829,9 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (value == "*") cx.marked = "keyword";
return cont(maybeAs);
}
function maybeMoreImports(type) {
if (type == ",") return cont(importSpec, maybeMoreImports)
}
function maybeAs(_type, value) {
if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
}
@@ -631,16 +840,13 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
}
function arrayLiteral(type) {
if (type == "]") return cont();
return pass(expressionNoComma, maybeArrayComprehension);
}
function maybeArrayComprehension(type) {
if (type == "for") return pass(comprehension, expect("]"));
if (type == ",") return cont(commasep(maybeexpressionNoComma, "]"));
return pass(commasep(expressionNoComma, "]"));
}
function comprehension(type) {
if (type == "for") return cont(forspec, comprehension);
if (type == "if") return cont(expression, comprehension);
function enumdef() {
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
}
function enummember() {
return pass(pattern, maybeAssign);
}
function isContinuedStatement(state, textAfter) {
@@ -649,6 +855,12 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
/[,.]/.test(textAfter.charAt(0));
}
function expressionAllowed(stream, state, backUp) {
return state.tokenize == tokenBase &&
/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
}
// Interface
return {
@@ -659,7 +871,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
context: parserConfig.localVars && {vars: parserConfig.localVars},
context: parserConfig.localVars && new Context(null, null, false),
indented: basecolumn || 0
};
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
@@ -682,21 +894,25 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
},
indent: function(state, textAfter) {
if (state.tokenize == tokenComment) return CodeMirror.Pass;
if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass;
if (state.tokenize != tokenBase) return 0;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
// Kludge to prevent 'maybelse' from blocking lexical scope pops
if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
var c = state.cc[i];
if (c == poplex) lexical = lexical.prev;
else if (c != maybeelse) break;
else if (c != maybeelse && c != popcontext) break;
}
if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
while ((lexical.type == "stat" || lexical.type == "form") &&
(firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
(top == maybeoperatorComma || top == maybeoperatorNoComma) &&
!/^[,\.=+\-*:?[\(]/.test(textAfter))))
lexical = lexical.prev;
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
lexical = lexical.prev;
var type = lexical.type, closing = firstChar == type;
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0);
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
else if (type == "form" && firstChar == "{") return lexical.indented;
else if (type == "form") return lexical.indented + indentUnit;
else if (type == "stat")
@@ -710,6 +926,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
blockCommentStart: jsonMode ? null : "/*",
blockCommentEnd: jsonMode ? null : "*/",
blockCommentContinue: jsonMode ? null : " * ",
lineComment: jsonMode ? null : "//",
fold: "brace",
closeBrackets: "()[]{}''\"\"``",
@@ -719,9 +936,9 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
jsonMode: jsonMode,
expressionAllowed: expressionAllowed,
skipExpression: function(state) {
var top = state.cc[state.cc.length - 1]
if (top == expression || top == expressionNoComma) state.cc.pop()
parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null))
}
};
});
@@ -733,9 +950,10 @@ CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/x-javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true});
CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true});
CodeMirror.defineMIME("application/json", { name: "javascript", json: true });
CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true });
CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true })
CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true });
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });

View File

@@ -1,72 +0,0 @@
<!doctype html>
<title>CodeMirror: JSON-LD mode</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../../doc/docs.css">
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="../../addon/edit/matchbrackets.js"></script>
<script src="../../addon/comment/continuecomment.js"></script>
<script src="../../addon/comment/comment.js"></script>
<script src="javascript.js"></script>
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
<div id="nav">
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"/></a>
<ul>
<li><a href="../../index.html">Home</a>
<li><a href="../../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a href="../index.html">Language modes</a>
<li><a class=active href="#">JSON-LD</a>
</ul>
</div>
<article>
<h2>JSON-LD mode</h2>
<div><textarea id="code" name="code">
{
"@context": {
"name": "http://schema.org/name",
"description": "http://schema.org/description",
"image": {
"@id": "http://schema.org/image",
"@type": "@id"
},
"geo": "http://schema.org/geo",
"latitude": {
"@id": "http://schema.org/latitude",
"@type": "xsd:float"
},
"longitude": {
"@id": "http://schema.org/longitude",
"@type": "xsd:float"
},
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"name": "The Empire State Building",
"description": "The Empire State Building is a 102-story landmark in New York City.",
"image": "http://www.civil.usherbrooke.ca/cours/gci215a/empire-state-building.jpg",
"geo": {
"latitude": "40.75",
"longitude": "73.98"
}
}
</textarea></div>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
matchBrackets: true,
autoCloseBrackets: true,
mode: "application/ld+json",
lineWrapping: true
});
</script>
<p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
</article>

View File

@@ -1,61 +0,0 @@
<!doctype html>
<title>CodeMirror: TypeScript mode</title>
<meta charset="utf-8"/>
<link rel=stylesheet href="../../doc/docs.css">
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="javascript.js"></script>
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
<div id=nav>
<a href="http://codemirror.net"><h1>CodeMirror</h1><img id=logo src="../../doc/logo.png"></a>
<ul>
<li><a href="../../index.html">Home</a>
<li><a href="../../doc/manual.html">Manual</a>
<li><a href="https://github.com/codemirror/codemirror">Code</a>
</ul>
<ul>
<li><a href="../index.html">Language modes</a>
<li><a class=active href="#">TypeScript</a>
</ul>
</div>
<article>
<h2>TypeScript mode</h2>
<div><textarea id="code" name="code">
class Greeter {
greeting: string;
constructor (message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
var greeter = new Greeter("world");
var button = document.createElement('button')
button.innerText = "Say Hello"
button.onclick = function() {
alert(greeter.greet())
}
document.body.appendChild(button)
</textarea></div>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
lineNumbers: true,
matchBrackets: true,
mode: "text/typescript"
});
</script>
<p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
</article>

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -26,13 +26,13 @@
}
CodeMirror.defineMode("jsx", function(config, modeConfig) {
var xmlMode = CodeMirror.getMode(config, {name: "xml", allowMissing: true, multilineTagIndentPastTag: false})
var xmlMode = CodeMirror.getMode(config, {name: "xml", allowMissing: true, multilineTagIndentPastTag: false, allowMissingTagName: true})
var jsMode = CodeMirror.getMode(config, modeConfig && modeConfig.base || "javascript")
function flatXMLIndent(state) {
var tagName = state.tagName
state.tagName = null
var result = xmlMode.indent(state, "")
var result = xmlMode.indent(state, "", "")
state.tagName = tagName
return result
}
@@ -103,10 +103,11 @@
}
function jsToken(stream, state, cx) {
if (stream.peek() == "<" && jsMode.expressionAllowed(stream, cx.state)) {
jsMode.skipExpression(cx.state)
state.context = new Context(CodeMirror.startState(xmlMode, jsMode.indent(cx.state, "")),
if (stream.peek() == "<" && !stream.match(/^<([^<>]|<[^>]*>)+,\s*>/, false) &&
jsMode.expressionAllowed(stream, cx.state)) {
state.context = new Context(CodeMirror.startState(xmlMode, jsMode.indent(cx.state, "", "")),
xmlMode, 0, state.context)
jsMode.skipExpression(cx.state)
return null
}

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -35,15 +35,6 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (modeCfg.maxBlockquoteDepth === undefined)
modeCfg.maxBlockquoteDepth = 0;
// Should underscores in words open/close em/strong?
if (modeCfg.underscoresBreakWords === undefined)
modeCfg.underscoresBreakWords = true;
// Use `fencedCodeBlocks` to configure fenced code blocks. false to
// disable, string to specify a precise regexp that the fence should
// match, and true to allow three or more backticks or tildes (as
// per CommonMark).
// Turn on task lists? ("- [ ] " and "- [x] ")
if (modeCfg.taskLists === undefined) modeCfg.taskLists = false;
@@ -51,6 +42,18 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (modeCfg.strikethrough === undefined)
modeCfg.strikethrough = false;
if (modeCfg.emoji === undefined)
modeCfg.emoji = false;
if (modeCfg.fencedCodeBlockHighlighting === undefined)
modeCfg.fencedCodeBlockHighlighting = true;
if (modeCfg.fencedCodeBlockDefaultMode === undefined)
modeCfg.fencedCodeBlockDefaultMode = 'text/plain';
if (modeCfg.xml === undefined)
modeCfg.xml = true;
// Allow token types to be overridden by user-provided token types.
if (modeCfg.tokenTypeOverrides === undefined)
modeCfg.tokenTypeOverrides = {};
@@ -63,7 +66,9 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
list2: "variable-3",
list3: "keyword",
hr: "hr",
image: "tag",
image: "image",
imageAltText: "image-alt-text",
imageMarker: "image-marker",
formatting: "formatting",
linkInline: "link",
linkEmail: "link",
@@ -71,7 +76,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
linkHref: "string",
em: "em",
strong: "strong",
strikethrough: "strikethrough"
strikethrough: "strikethrough",
emoji: "builtin"
};
for (var tokenType in tokenTypes) {
@@ -81,14 +87,15 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/
, ulRE = /^[*\-+]\s+/
, olRE = /^[0-9]+([.)])\s+/
, taskListRE = /^\[(x| )\](?=\s)/ // Must follow ulRE or olRE
, listRE = /^(?:[*\-+]|^[0-9]+([.)]))\s+/
, taskListRE = /^\[(x| )\](?=\s)/i // Must follow listRE
, atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/
, setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/
, textRE = /^[^#!\[\]*_\\<>` "'(~]+/
, fencedCodeRE = new RegExp("^(" + (modeCfg.fencedCodeBlocks === true ? "~~~+|```+" : modeCfg.fencedCodeBlocks) +
")[ \\t]*([\\w+#\-]*)");
, setextHeaderRE = /^ {0,3}(?:\={1,}|-{2,})\s*$/
, textRE = /^[^#!\[\]*_\\<>` "'(~:]+/
, fencedCodeRE = /^(~~~+|```+)[ \t]*([\w\/+#-]*)[^\n`]*$/
, linkDefRE = /^\s*\[[^\]]+?\]:.*$/ // naive link-definition
, punctuation = /[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/
, expandedTab = " " // CommonMark specifies tab as 4 spaces
function switchInline(stream, state, f) {
state.f = state.inline = f;
@@ -109,6 +116,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
function blankLine(state) {
// Reset linkTitle state
state.linkTitle = false;
state.linkHref = false;
state.linkText = false;
// Reset EM state
state.em = false;
// Reset STRONG state
@@ -119,94 +128,106 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
state.quote = 0;
// Reset state.indentedCode
state.indentedCode = false;
if (htmlModeMissing && state.f == htmlBlock) {
state.f = inlineNormal;
state.block = blockNormal;
if (state.f == htmlBlock) {
var exit = htmlModeMissing
if (!exit) {
var inner = CodeMirror.innerMode(htmlMode, state.htmlState)
exit = inner.mode.name == "xml" && inner.state.tagStart === null &&
(!inner.state.context && inner.state.tokenize.isInText)
}
if (exit) {
state.f = inlineNormal;
state.block = blockNormal;
state.htmlState = null;
}
}
// Reset state.trailingSpace
state.trailingSpace = 0;
state.trailingSpaceNewLine = false;
// Mark this line as blank
state.prevLine = state.thisLine
state.thisLine = null
state.thisLine = {stream: null}
return null;
}
function blockNormal(stream, state) {
var sol = stream.sol();
var prevLineIsList = state.list !== false,
prevLineIsIndentedCode = state.indentedCode;
var firstTokenOnLine = stream.column() === state.indentation;
var prevLineLineIsEmpty = lineIsEmpty(state.prevLine.stream);
var prevLineIsIndentedCode = state.indentedCode;
var prevLineIsHr = state.prevLine.hr;
var prevLineIsList = state.list !== false;
var maxNonCodeIndentation = (state.listStack[state.listStack.length - 1] || 0) + 3;
state.indentedCode = false;
if (prevLineIsList) {
if (state.indentationDiff >= 0) { // Continued list
if (state.indentationDiff < 4) { // Only adjust indentation if *not* a code block
state.indentation -= state.indentationDiff;
var lineIndentation = state.indentation;
// compute once per line (on first token)
if (state.indentationDiff === null) {
state.indentationDiff = state.indentation;
if (prevLineIsList) {
state.list = null;
// While this list item's marker's indentation is less than the deepest
// list item's content's indentation,pop the deepest list item
// indentation off the stack, and update block indentation state
while (lineIndentation < state.listStack[state.listStack.length - 1]) {
state.listStack.pop();
if (state.listStack.length) {
state.indentation = state.listStack[state.listStack.length - 1];
// less than the first list's indent -> the line is no longer a list
} else {
state.list = false;
}
}
if (state.list !== false) {
state.indentationDiff = lineIndentation - state.listStack[state.listStack.length - 1]
}
state.list = null;
} else if (state.indentation > 0) {
state.list = null;
} else { // No longer a list
state.list = false;
}
}
// not comprehensive (currently only for setext detection purposes)
var allowsInlineContinuation = (
!prevLineLineIsEmpty && !prevLineIsHr && !state.prevLine.header &&
(!prevLineIsList || !prevLineIsIndentedCode) &&
!state.prevLine.fencedCodeEnd
);
var isHr = (state.list === false || prevLineIsHr || prevLineLineIsEmpty) &&
state.indentation <= maxNonCodeIndentation && stream.match(hrRE);
var match = null;
if (state.indentationDiff >= 4) {
if (state.indentationDiff >= 4 && (prevLineIsIndentedCode || state.prevLine.fencedCodeEnd ||
state.prevLine.header || prevLineLineIsEmpty)) {
stream.skipToEnd();
if (prevLineIsIndentedCode || lineIsEmpty(state.prevLine)) {
state.indentation -= 4;
state.indentedCode = true;
return tokenTypes.code;
} else {
return null;
}
state.indentedCode = true;
return tokenTypes.code;
} else if (stream.eatSpace()) {
return null;
} else if ((match = stream.match(atxHeaderRE)) && match[1].length <= 6) {
} else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(atxHeaderRE)) && match[1].length <= 6) {
state.quote = 0;
state.header = match[1].length;
state.thisLine.header = true;
if (modeCfg.highlightFormatting) state.formatting = "header";
state.f = state.inline;
return getType(state);
} else if (!lineIsEmpty(state.prevLine) && !state.quote && !prevLineIsList &&
!prevLineIsIndentedCode && (match = stream.match(setextHeaderRE))) {
state.header = match[0].charAt(0) == '=' ? 1 : 2;
if (modeCfg.highlightFormatting) state.formatting = "header";
state.f = state.inline;
return getType(state);
} else if (stream.eat('>')) {
state.quote = sol ? 1 : state.quote + 1;
} else if (state.indentation <= maxNonCodeIndentation && stream.eat('>')) {
state.quote = firstTokenOnLine ? 1 : state.quote + 1;
if (modeCfg.highlightFormatting) state.formatting = "quote";
stream.eatSpace();
return getType(state);
} else if (stream.peek() === '[') {
return switchInline(stream, state, footnoteLink);
} else if (stream.match(hrRE, true)) {
state.hr = true;
return tokenTypes.hr;
} else if ((lineIsEmpty(state.prevLine) || prevLineIsList) && (stream.match(ulRE, false) || stream.match(olRE, false))) {
var listType = null;
if (stream.match(ulRE, true)) {
listType = 'ul';
} else {
stream.match(olRE, true);
listType = 'ol';
}
state.indentation = stream.column() + stream.current().length;
state.list = true;
} else if (!isHr && !state.setext && firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(listRE))) {
var listType = match[1] ? "ol" : "ul";
// While this list item's marker's indentation
// is less than the deepest list item's content's indentation,
// pop the deepest list item indentation off the stack.
while (state.listStack && stream.column() < state.listStack[state.listStack.length - 1]) {
state.listStack.pop();
}
state.indentation = lineIndentation + stream.current().length;
state.list = true;
state.quote = 0;
// Add this list item's content's indentation to the stack
state.listStack.push(state.indentation);
// Reset inline styles which shouldn't propagate across list items
state.em = false;
state.strong = false;
state.code = false;
state.strikethrough = false;
if (modeCfg.taskLists && stream.match(taskListRE, false)) {
state.taskList = true;
@@ -214,15 +235,47 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
state.f = state.inline;
if (modeCfg.highlightFormatting) state.formatting = ["list", "list-" + listType];
return getType(state);
} else if (modeCfg.fencedCodeBlocks && (match = stream.match(fencedCodeRE, true))) {
state.fencedChars = match[1]
} else if (firstTokenOnLine && state.indentation <= maxNonCodeIndentation && (match = stream.match(fencedCodeRE, true))) {
state.quote = 0;
state.fencedEndRE = new RegExp(match[1] + "+ *$");
// try switching mode
state.localMode = getMode(match[2]);
state.localMode = modeCfg.fencedCodeBlockHighlighting && getMode(match[2] || modeCfg.fencedCodeBlockDefaultMode );
if (state.localMode) state.localState = CodeMirror.startState(state.localMode);
state.f = state.block = local;
if (modeCfg.highlightFormatting) state.formatting = "code-block";
state.code = -1
return getType(state);
// SETEXT has lowest block-scope precedence after HR, so check it after
// the others (code, blockquote, list...)
} else if (
// if setext set, indicates line after ---/===
state.setext || (
// line before ---/===
(!allowsInlineContinuation || !prevLineIsList) && !state.quote && state.list === false &&
!state.code && !isHr && !linkDefRE.test(stream.string) &&
(match = stream.lookAhead(1)) && (match = match.match(setextHeaderRE))
)
) {
if ( !state.setext ) {
state.header = match[0].charAt(0) == '=' ? 1 : 2;
state.setext = state.header;
} else {
state.header = state.setext;
// has no effect on type so we can reset it now
state.setext = 0;
stream.skipToEnd();
if (modeCfg.highlightFormatting) state.formatting = "header";
}
state.thisLine.header = true;
state.f = state.inline;
return getType(state);
} else if (isHr) {
stream.skipToEnd();
state.hr = true;
state.thisLine.hr = true;
return tokenTypes.hr;
} else if (stream.peek() === '[') {
return switchInline(stream, state, footnoteLink);
}
return switchInline(stream, state, state.inline);
@@ -244,10 +297,21 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
function local(stream, state) {
if (state.fencedChars && stream.match(state.fencedChars, false)) {
var currListInd = state.listStack[state.listStack.length - 1] || 0;
var hasExitedList = state.indentation < currListInd;
var maxFencedEndInd = currListInd + 3;
if (state.fencedEndRE && state.indentation <= maxFencedEndInd && (hasExitedList || stream.match(state.fencedEndRE))) {
if (modeCfg.highlightFormatting) state.formatting = "code-block";
var returnType;
if (!hasExitedList) returnType = getType(state)
state.localMode = state.localState = null;
state.f = state.block = leavingLocal;
return null;
state.block = blockNormal;
state.f = inlineNormal;
state.fencedEndRE = null;
state.code = 0
state.thisLine.fencedCodeEnd = true;
if (hasExitedList) return switchBlock(stream, state, state.block);
return returnType;
} else if (state.localMode) {
return state.localMode.token(stream, state.localState);
} else {
@@ -256,18 +320,6 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
}
function leavingLocal(stream, state) {
stream.match(state.fencedChars);
state.block = blockNormal;
state.f = inlineNormal;
state.fencedChars = null;
if (modeCfg.highlightFormatting) state.formatting = "code-block";
state.code = 1
var returnType = getType(state);
state.code = 0
return returnType;
}
// Inline
function getType(state) {
var styles = [];
@@ -311,8 +363,12 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (state.strong) { styles.push(tokenTypes.strong); }
if (state.em) { styles.push(tokenTypes.em); }
if (state.strikethrough) { styles.push(tokenTypes.strikethrough); }
if (state.emoji) { styles.push(tokenTypes.emoji); }
if (state.linkText) { styles.push(tokenTypes.linkText); }
if (state.code) { styles.push(tokenTypes.code); }
if (state.image) { styles.push(tokenTypes.image); }
if (state.imageAltText) { styles.push(tokenTypes.imageAltText, "link"); }
if (state.imageMarker) { styles.push(tokenTypes.imageMarker); }
}
if (state.header) { styles.push(tokenTypes.header, tokenTypes.header + "-" + state.header); }
@@ -366,7 +422,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
if (state.taskList) {
var taskOpen = stream.match(taskListRE, true)[1] !== "x";
var taskOpen = stream.match(taskListRE, true)[1] === " ";
if (taskOpen) state.taskOpen = true;
else state.taskClosed = true;
if (modeCfg.highlightFormatting) state.formatting = "task";
@@ -382,9 +438,6 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return getType(state);
}
// Get sol() value now, before character is consumed
var sol = stream.sol();
var ch = stream.next();
// Matches link titles present on next line
@@ -394,7 +447,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (ch === '(') {
matchCh = ')';
}
matchCh = (matchCh+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
matchCh = (matchCh+'').replace(/([.?*+^\[\]\\(){}|-])/g, "\\$1");
var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh;
if (stream.match(new RegExp(regex), true)) {
return tokenTypes.linkHref;
@@ -407,7 +460,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (modeCfg.highlightFormatting) state.formatting = "code";
stream.eatWhile('`');
var count = stream.current().length
if (state.code == 0) {
if (state.code == 0 && (!state.quote || count == 1)) {
state.code = count
return getType(state)
} else if (count == state.code) { // Must be exact
@@ -432,22 +485,40 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) {
stream.match(/\[[^\]]*\]/);
state.inline = state.f = linkHref;
return tokenTypes.image;
state.imageMarker = true;
state.image = true;
if (modeCfg.highlightFormatting) state.formatting = "image";
return getType(state);
}
if (ch === '[' && stream.match(/[^\]]*\](\(.*\)| ?\[.*?\])/, false)) {
if (ch === '[' && state.imageMarker && stream.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/, false)) {
state.imageMarker = false;
state.imageAltText = true
if (modeCfg.highlightFormatting) state.formatting = "image";
return getType(state);
}
if (ch === ']' && state.imageAltText) {
if (modeCfg.highlightFormatting) state.formatting = "image";
var type = getType(state);
state.imageAltText = false;
state.image = false;
state.inline = state.f = linkHref;
return type;
}
if (ch === '[' && !state.image) {
if (state.linkText && stream.match(/^.*?\]/)) return getType(state)
state.linkText = true;
if (modeCfg.highlightFormatting) state.formatting = "link";
return getType(state);
}
if (ch === ']' && state.linkText && stream.match(/\(.*?\)| ?\[.*?\]/, false)) {
if (ch === ']' && state.linkText) {
if (modeCfg.highlightFormatting) state.formatting = "link";
var type = getType(state);
state.linkText = false;
state.inline = state.f = linkHref;
state.inline = state.f = stream.match(/\(.*?\)| ?\[.*?\]/, false) ? linkHref : inlineNormal
return type;
}
@@ -475,7 +546,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return type + tokenTypes.linkEmail;
}
if (ch === '<' && stream.match(/^(!--|\w)/, false)) {
if (modeCfg.xml && ch === '<' && stream.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i, false)) {
var end = stream.string.indexOf(">", stream.pos);
if (end != -1) {
var atts = stream.string.substring(stream.start, end);
@@ -486,44 +557,37 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return switchBlock(stream, state, htmlBlock);
}
if (ch === '<' && stream.match(/^\/\w*?>/)) {
if (modeCfg.xml && ch === '<' && stream.match(/^\/\w*?>/)) {
state.md_inside = false;
return "tag";
}
var ignoreUnderscore = false;
if (!modeCfg.underscoresBreakWords) {
if (ch === '_' && stream.peek() !== '_' && stream.match(/(\w)/, false)) {
var prevPos = stream.pos - 2;
if (prevPos >= 0) {
var prevCh = stream.string.charAt(prevPos);
if (prevCh !== '_' && prevCh.match(/(\w)/, false)) {
ignoreUnderscore = true;
}
}
} else if (ch === "*" || ch === "_") {
var len = 1, before = stream.pos == 1 ? " " : stream.string.charAt(stream.pos - 2)
while (len < 3 && stream.eat(ch)) len++
var after = stream.peek() || " "
// See http://spec.commonmark.org/0.27/#emphasis-and-strong-emphasis
var leftFlanking = !/\s/.test(after) && (!punctuation.test(after) || /\s/.test(before) || punctuation.test(before))
var rightFlanking = !/\s/.test(before) && (!punctuation.test(before) || /\s/.test(after) || punctuation.test(after))
var setEm = null, setStrong = null
if (len % 2) { // Em
if (!state.em && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before)))
setEm = true
else if (state.em == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after)))
setEm = false
}
}
if (ch === '*' || (ch === '_' && !ignoreUnderscore)) {
if (sol && stream.peek() === ' ') {
// Do nothing, surrounded by newline and space
} else if (state.strong === ch && stream.eat(ch)) { // Remove STRONG
if (modeCfg.highlightFormatting) state.formatting = "strong";
var t = getType(state);
state.strong = false;
return t;
} else if (!state.strong && stream.eat(ch)) { // Add STRONG
state.strong = ch;
if (modeCfg.highlightFormatting) state.formatting = "strong";
return getType(state);
} else if (state.em === ch) { // Remove EM
if (modeCfg.highlightFormatting) state.formatting = "em";
var t = getType(state);
state.em = false;
return t;
} else if (!state.em) { // Add EM
state.em = ch;
if (modeCfg.highlightFormatting) state.formatting = "em";
return getType(state);
if (len > 1) { // Strong
if (!state.strong && leftFlanking && (ch === "*" || !rightFlanking || punctuation.test(before)))
setStrong = true
else if (state.strong == ch && rightFlanking && (ch === "*" || !leftFlanking || punctuation.test(after)))
setStrong = false
}
if (setStrong != null || setEm != null) {
if (modeCfg.highlightFormatting) state.formatting = setEm == null ? "strong" : setStrong == null ? "em" : "strong em"
if (setEm === true) state.em = ch
if (setStrong === true) state.strong = ch
var t = getType(state)
if (setEm === false) state.em = false
if (setStrong === false) state.strong = false
return t
}
} else if (ch === ' ') {
if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces
@@ -548,7 +612,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return getType(state);
}
} else if (ch === ' ') {
if (stream.match(/^~~/, true)) { // Probably surrounded by space
if (stream.match('~~', true)) { // Probably surrounded by space
if (stream.peek() === ' ') { // Surrounded by spaces, ignore
return getType(state);
} else { // Not surrounded by spaces, back up pointer
@@ -558,8 +622,16 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
}
if (modeCfg.emoji && ch === ":" && stream.match(/^(?:[a-z_\d+][a-z_\d+-]*|\-[a-z_\d+][a-z_\d+-]*):/)) {
state.emoji = true;
if (modeCfg.highlightFormatting) state.formatting = "emoji";
var retType = getType(state);
state.emoji = false;
return retType;
}
if (ch === ' ') {
if (stream.match(/ +$/, false)) {
if (stream.match(/^ +$/, false)) {
state.trailingSpace++;
} else if (state.trailingSpace) {
state.trailingSpaceNewLine = true;
@@ -596,7 +668,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
var ch = stream.next();
if (ch === '(' || ch === '[') {
state.f = state.inline = getLinkHrefInside(ch === "(" ? ")" : "]", 0);
state.f = state.inline = getLinkHrefInside(ch === "(" ? ")" : "]");
if (modeCfg.highlightFormatting) state.formatting = "link-string";
state.linkHref = true;
return getType(state);
@@ -606,7 +678,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
var linkRE = {
")": /^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,
"]": /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\\]]|\\.)*\])*?(?=\])/
"]": /^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/
}
function getLinkHrefInside(endChar) {
@@ -639,7 +711,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
function footnoteLinkInside(stream, state) {
if (stream.match(/^\]:/, true)) {
if (stream.match(']:', true)) {
state.f = state.inline = footnoteUrl;
if (modeCfg.highlightFormatting) state.formatting = "link";
var returnType = getType(state);
@@ -663,7 +735,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (stream.peek() === undefined) { // End of line, set flag to check next line
state.linkTitle = true;
} else { // More content on line, check if link title
stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true);
stream.match(/^(?:\s+(?:"(?:[^"\\]|\\.)+"|'(?:[^'\\]|\\.)+'|\((?:[^)\\]|\\.)+\)))?/, true);
}
state.f = state.inline = inlineNormal;
return tokenTypes.linkHref + " url";
@@ -674,8 +746,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return {
f: blockNormal,
prevLine: null,
thisLine: null,
prevLine: {stream: null},
thisLine: {stream: null},
block: blockNormal,
htmlState: null,
@@ -692,6 +764,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
em: false,
strong: false,
header: 0,
setext: 0,
hr: false,
taskList: false,
list: false,
@@ -700,7 +773,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
trailingSpace: 0,
trailingSpaceNewLine: false,
strikethrough: false,
fencedChars: null
emoji: false,
fencedEndRE: null
};
},
@@ -721,12 +795,16 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
inline: s.inline,
text: s.text,
formatting: false,
linkText: s.linkText,
linkTitle: s.linkTitle,
linkHref: s.linkHref,
code: s.code,
em: s.em,
strong: s.strong,
strikethrough: s.strikethrough,
emoji: s.emoji,
header: s.header,
setext: s.setext,
hr: s.hr,
taskList: s.taskList,
list: s.list,
@@ -736,7 +814,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
trailingSpace: s.trailingSpace,
trailingSpaceNewLine: s.trailingSpaceNewLine,
md_inside: s.md_inside,
fencedChars: s.fencedChars
fencedEndRE: s.fencedEndRE
};
},
@@ -745,21 +823,17 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
// Reset state.formatting
state.formatting = false;
if (stream != state.thisLine) {
var forceBlankLine = state.header || state.hr;
// Reset state.header and state.hr
if (stream != state.thisLine.stream) {
state.header = 0;
state.hr = false;
if (stream.match(/^\s*$/, true) || forceBlankLine) {
if (stream.match(/^\s*$/, true)) {
blankLine(state);
if (!forceBlankLine) return null
state.prevLine = null
return null;
}
state.prevLine = state.thisLine
state.thisLine = stream
state.thisLine = {stream: stream}
// Reset state.taskList
state.taskList = false;
@@ -768,11 +842,15 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
state.trailingSpace = 0;
state.trailingSpaceNewLine = false;
state.f = state.block;
var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length;
state.indentationDiff = Math.min(indentation - state.indentation, 4);
state.indentation = state.indentation + state.indentationDiff;
if (indentation > 0) return null;
if (!state.localState) {
state.f = state.block;
if (state.f != htmlBlock) {
var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, expandedTab).length;
state.indentation = indentation;
state.indentationDiff = null;
if (indentation > 0) return null;
}
}
}
return state.f(stream, state);
},
@@ -783,15 +861,26 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return {state: state, mode: mode};
},
indent: function(state, textAfter, line) {
if (state.block == htmlBlock && htmlMode.indent) return htmlMode.indent(state.htmlState, textAfter, line)
if (state.localState && state.localMode.indent) return state.localMode.indent(state.localState, textAfter, line)
return CodeMirror.Pass
},
blankLine: blankLine,
getType: getType,
blockCommentStart: "<!--",
blockCommentEnd: "-->",
closeBrackets: "()[]{}''\"\"``",
fold: "markdown"
};
return mode;
}, "xml");
CodeMirror.defineMIME("text/markdown", "markdown");
CodeMirror.defineMIME("text/x-markdown", "markdown");
});

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -261,7 +261,7 @@ CodeMirror.defineMode("pug", function (config) {
}
return 'variable';
}
if (stream.match(/^\+#{/, false)) {
if (stream.match('+#{', false)) {
stream.next();
state.mixinCallAfter = true;
return interpolation(stream, state);
@@ -545,12 +545,12 @@ CodeMirror.defineMode("pug", function (config) {
|| javaScriptArguments(stream, state)
|| callArguments(stream, state)
|| yieldStatement(stream, state)
|| doctype(stream, state)
|| yieldStatement(stream)
|| doctype(stream)
|| interpolation(stream, state)
|| caseStatement(stream, state)
|| when(stream, state)
|| defaultStatement(stream, state)
|| defaultStatement(stream)
|| extendsStatement(stream, state)
|| append(stream, state)
|| prepend(stream, state)
@@ -565,16 +565,16 @@ CodeMirror.defineMode("pug", function (config) {
|| tag(stream, state)
|| filter(stream, state)
|| code(stream, state)
|| id(stream, state)
|| className(stream, state)
|| id(stream)
|| className(stream)
|| attrs(stream, state)
|| attributesBlock(stream, state)
|| indent(stream, state)
|| indent(stream)
|| text(stream, state)
|| comment(stream, state)
|| colon(stream, state)
|| colon(stream)
|| dot(stream, state)
|| fail(stream, state);
|| fail(stream);
return tok === true ? null : tok;
}

View File

@@ -1,17 +1,23 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
mod(require("../../lib/codemirror"), require("../css/css"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
define(["../../lib/codemirror", "../css/css"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("sass", function(config) {
var cssMode = CodeMirror.mimeModes["text/css"];
var propertyKeywords = cssMode.propertyKeywords || {},
colorKeywords = cssMode.colorKeywords || {},
valueKeywords = cssMode.valueKeywords || {},
fontProperties = cssMode.fontProperties || {};
function tokenRegexp(words) {
return new RegExp("^" + words.join("|"));
}
@@ -25,6 +31,12 @@ CodeMirror.defineMode("sass", function(config) {
var pseudoElementsRegexp = /^::?[a-zA-Z_][\w\-]*/;
var word;
function isEndLine(stream) {
return !stream.peek() || stream.match(/\s+$/, false);
}
function urlTokens(stream, state) {
var ch = stream.peek();
@@ -76,6 +88,9 @@ CodeMirror.defineMode("sass", function(config) {
if (endingString) {
if (nextChar !== quote && greedy) { stream.next(); }
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
state.tokenizer = tokenBase;
return "string";
} else if (nextChar === "#" && peekChar === "{") {
@@ -147,14 +162,20 @@ CodeMirror.defineMode("sass", function(config) {
// first half i.e. before : for key-value pairs
// including selectors
if (ch === "-") {
if (stream.match(/^-\w+-/)) {
return "meta";
}
}
if (ch === ".") {
stream.next();
if (stream.match(/^[\w-]+/)) {
indent(state);
return "atom";
return "qualifier";
} else if (stream.peek() === "#") {
indent(state);
return "atom";
return "tag";
}
}
@@ -163,11 +184,11 @@ CodeMirror.defineMode("sass", function(config) {
// ID selectors
if (stream.match(/^[\w-]+/)) {
indent(state);
return "atom";
return "builtin";
}
if (stream.peek() === "#") {
indent(state);
return "atom";
return "tag";
}
}
@@ -210,7 +231,7 @@ CodeMirror.defineMode("sass", function(config) {
}
if(ch === "@"){
if(stream.match(/@extend/)){
if(stream.match('@extend')){
if(!stream.match(/\s*[\w]/))
dedent(state);
}
@@ -220,37 +241,48 @@ CodeMirror.defineMode("sass", function(config) {
// Indent Directives
if (stream.match(/^@(else if|if|media|else|for|each|while|mixin|function)/)) {
indent(state);
return "meta";
return "def";
}
// Other Directives
if (ch === "@") {
stream.next();
stream.eatWhile(/[\w-]/);
return "meta";
return "def";
}
if (stream.eatWhile(/[\w-]/)){
if(stream.match(/ *: *[\w-\+\$#!\("']/,false)){
return "property";
word = stream.current().toLowerCase();
var prop = state.prevProp + "-" + word;
if (propertyKeywords.hasOwnProperty(prop)) {
return "property";
} else if (propertyKeywords.hasOwnProperty(word)) {
state.prevProp = word;
return "property";
} else if (fontProperties.hasOwnProperty(word)) {
return "property";
}
return "tag";
}
else if(stream.match(/ *:/,false)){
indent(state);
state.cursorHalf = 1;
return "atom";
state.prevProp = stream.current().toLowerCase();
return "property";
}
else if(stream.match(/ *,/,false)){
return "atom";
return "tag";
}
else{
indent(state);
return "atom";
return "tag";
}
}
if(ch === ":"){
if (stream.match(pseudoElementsRegexp)){ // could be a pseudo-element
return "keyword";
return "variable-3";
}
stream.next();
state.cursorHalf=1;
@@ -264,7 +296,7 @@ CodeMirror.defineMode("sass", function(config) {
stream.next();
// Hex numbers
if (stream.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)){
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "number";
@@ -273,7 +305,7 @@ CodeMirror.defineMode("sass", function(config) {
// Numbers
if (stream.match(/^-?[0-9\.]+/)){
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "number";
@@ -281,14 +313,14 @@ CodeMirror.defineMode("sass", function(config) {
// Units
if (stream.match(/^(px|em|in)\b/)){
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "unit";
}
if (stream.match(keywordsRegexp)){
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "keyword";
@@ -296,7 +328,7 @@ CodeMirror.defineMode("sass", function(config) {
if (stream.match(/^url/) && stream.peek() === "(") {
state.tokenizer = urlTokens;
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "atom";
@@ -306,23 +338,21 @@ CodeMirror.defineMode("sass", function(config) {
if (ch === "$") {
stream.next();
stream.eatWhile(/[\w-]/);
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "variable-3";
return "variable-2";
}
// bang character for !important, !default, etc.
if (ch === "!") {
stream.next();
if(!stream.peek()){
state.cursorHalf = 0;
}
state.cursorHalf = 0;
return stream.match(/^[\w]+/) ? "keyword": "operator";
}
if (stream.match(opRegexp)){
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "operator";
@@ -330,14 +360,24 @@ CodeMirror.defineMode("sass", function(config) {
// attributes
if (stream.eatWhile(/[\w-]/)) {
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
}
return "attribute";
word = stream.current().toLowerCase();
if (valueKeywords.hasOwnProperty(word)) {
return "atom";
} else if (colorKeywords.hasOwnProperty(word)) {
return "keyword";
} else if (propertyKeywords.hasOwnProperty(word)) {
state.prevProp = stream.current().toLowerCase();
return "property";
} else {
return "tag";
}
}
//stream.eatSpace();
if(!stream.peek()){
if (isEndLine(stream)) {
state.cursorHalf = 0;
return null;
}
@@ -405,9 +445,14 @@ CodeMirror.defineMode("sass", function(config) {
indent: function(state) {
return state.scopes[0].offset;
}
},
blockCommentStart: "/*",
blockCommentEnd: "*/",
lineComment: "//",
fold: "indent"
};
});
}, "css");
CodeMirror.defineMIME("text/x-sass", "sass");

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
@@ -52,6 +52,7 @@ var xmlConfig = {
doNotIndent: {},
allowUnquoted: false,
allowMissing: false,
allowMissingTagName: false,
caseFold: false
}
@@ -162,8 +163,9 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
stream.next();
}
return style;
};
}
}
function doctype(depth) {
return function(stream, state) {
var ch;
@@ -185,9 +187,13 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
};
}
function lower(tagName) {
return tagName && tagName.toLowerCase();
}
function Context(state, tagName, startOfLine) {
this.prev = state.context;
this.tagName = tagName;
this.tagName = tagName || "";
this.indent = state.indented;
this.startOfLine = startOfLine;
if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent))
@@ -203,8 +209,8 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
return;
}
parentTagName = state.context.tagName;
if (!config.contextGrabbers.hasOwnProperty(parentTagName) ||
!config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
if (!config.contextGrabbers.hasOwnProperty(lower(parentTagName)) ||
!config.contextGrabbers[lower(parentTagName)].hasOwnProperty(lower(nextTagName))) {
return;
}
popContext(state);
@@ -226,6 +232,9 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
state.tagName = stream.current();
setStyle = "tag";
return attrState;
} else if (config.allowMissingTagName && type == "endTag") {
setStyle = "tag bracket";
return attrState(type, stream, state);
} else {
setStyle = "error";
return tagNameState;
@@ -235,7 +244,7 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
if (type == "word") {
var tagName = stream.current();
if (state.context && state.context.tagName != tagName &&
config.implicitlyClosed.hasOwnProperty(state.context.tagName))
config.implicitlyClosed.hasOwnProperty(lower(state.context.tagName)))
popContext(state);
if ((state.context && state.context.tagName == tagName) || config.matchClosing === false) {
setStyle = "tag";
@@ -244,6 +253,9 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
setStyle = "tag error";
return closeStateErr;
}
} else if (config.allowMissingTagName && type == "endTag") {
setStyle = "tag bracket";
return closeState(type, stream, state);
} else {
setStyle = "error";
return closeStateErr;
@@ -271,7 +283,7 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
var tagName = state.tagName, tagStart = state.tagStart;
state.tagName = state.tagStart = null;
if (type == "selfcloseTag" ||
config.autoSelfClosers.hasOwnProperty(tagName)) {
config.autoSelfClosers.hasOwnProperty(lower(tagName))) {
maybePopContext(state, tagName);
} else {
maybePopContext(state, tagName);
@@ -351,7 +363,7 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
if (context.tagName == tagAfter[2]) {
context = context.prev;
break;
} else if (config.implicitlyClosed.hasOwnProperty(context.tagName)) {
} else if (config.implicitlyClosed.hasOwnProperty(lower(context.tagName))) {
context = context.prev;
} else {
break;
@@ -359,8 +371,8 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
}
} else if (tagAfter) { // Opening tag spotted
while (context) {
var grabbers = config.contextGrabbers[context.tagName];
if (grabbers && grabbers.hasOwnProperty(tagAfter[2]))
var grabbers = config.contextGrabbers[lower(context.tagName)];
if (grabbers && grabbers.hasOwnProperty(lower(tagAfter[2])))
context = context.prev;
else
break;
@@ -382,6 +394,17 @@ CodeMirror.defineMode("xml", function(editorConf, config_) {
skipAttribute: function(state) {
if (state.state == attrValueState)
state.state = attrState
},
xmlCurrentTag: function(state) {
return state.tagName ? {name: state.tagName, close: state.type == "closeTag"} : null
},
xmlCurrentContext: function(state) {
var context = []
for (var cx = state.context; cx; cx = cx.prev)
context.push(cx.tagName)
return context.reverse()
}
};
});

30
src/lib/hint.min.css vendored

File diff suppressed because one or more lines are too long

View File

@@ -95,9 +95,14 @@ export const jsLibs = [
type: 'js'
},
{
url: 'https://cdn.tailwindcss.com/3.3.1',
url: 'https://cdn.tailwindcss.com/3.4.3',
label: 'Tailwind 3',
type: 'js'
},
{
url: 'https://unpkg.com/kaboom@3000/dist/kaboom.js',
label: 'Kaboom',
type: 'js'
}
];
export const cssLibs = [
@@ -128,7 +133,7 @@ export const cssLibs = [
},
{
url: 'https://cdnjs.cloudflare.com/ajax/libs/hint.css/2.7.0/hint.min.css',
url: 'https://cdnjs.cloudflare.com/ajax/libs/hint.css/3.0.0/hint.min.css',
label: 'Hint.css',
type: 'css'
},

View File

@@ -1,7 +1,7 @@
{
"name": "Web Maker",
"version": "5.2.0",
"manifest_version": 3,
"version": "6.0.0",
"manifest_version": 2,
"description": "Blazing fast & offline playground for your web experiments",
"homepage_url": "https://webmaker.app",
"permissions": ["storage", "tabs"],
@@ -9,7 +9,7 @@
"host_permissions": ["<all_urls>"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'",
"sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' filesystem: http://localhost:* https://localhost:* https://apis.google.com https://ajax.googleapis.com https://code.jquery.com https://cdnjs.cloudflare.com https://unpkg.com https://maxcdn.com https://cdn77.com https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net/ https://*.stripe.com/ https://builds.framerjs.com/ https://rawgit.com https://wzrd.in https://www.gstatic.com https://semantic-ui.com https://www.google-analytics.com https://cdn.tailwindcss.com 'unsafe-inline' 'unsafe-eval'; child-src 'self';"
"sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' filesystem: http://localhost:* https://localhost:* https://apis.google.com https://ajax.googleapis.com https://code.jquery.com https://cdnjs.cloudflare.com https://unpkg.com https://maxcdn.com https://cdn77.com https://maxcdn.bootstrapcdn.com https://cdn.jsdelivr.net/ https://*.stripe.com/ https://builds.framerjs.com/ https://rawgit.com https://wzrd.in https://www.gstatic.com https://semantic-ui.com https://www.google-analytics.com https://cdn.tailwindcss.com https://www.googletagmanager.com 'unsafe-inline' 'unsafe-eval'; child-src 'self';"
},
"options_ui": {
"page": "options.html",

View File

@@ -35,7 +35,7 @@
<body>
<h3>
Settings
<span style="opacity: 0.6; font-size: 0.7em"> v5.2.0</span>
<span style="opacity: 0.6; font-size: 0.7em"> v6.0.0</span>
</h3>
<form name="optionsForm">
<label>

View File

@@ -3,16 +3,33 @@
--color-text-dark-1: #b3aec4;
--color-bg: #252637;
--color-popup: #3a2b63;
--color-overlay: rgb(0 0 0 / 40%);
--color-close-btn: #d12b4a;
--code-font-size: 16px;
--color-button: #d3a447;
--color-button: #e3ba26;
--color-focus-outline: #d3a447;
--color-form-control-bg: #2c214b;
--color-heading: #fff;
--color-text-light: #b0a5d6;
--color-text-lightest-final: #787090;
--clr-brand: purple;
--color-pro-1: #1fffb3;
--color-pro-2: #f2ff00;
--color-btn-hover-1: hsl(53.35deg 100% 50%);
--color-btn-hover-2: hsl(38.96deg 100% 50%);
--footer-height: 37px;
--console-height: 32px;
--duration-modal-show: 0.3s;
--duration-modal-overlay-show: 0.2s;
--zindex-modal-overlay: 5;
--zindex-footer: 6;
--zindex-modal: 2000;
}
html {
@@ -85,6 +102,7 @@ p {
button {
font-family: inherit;
font-size: 100%;
cursor: pointer;
}
.hide {
@@ -120,11 +138,13 @@ button {
.ai-c {
align-items: center;
}
.flex-h-center {
.flex-h-center,
.jc-c {
justify-content: center;
}
.flex-h-end {
.flex-h-end,
.jc-fe {
justify-content: flex-end;
}
@@ -213,6 +233,10 @@ button {
color: #f7ae2d;
}
.para {
max-width: 60ch;
}
@media screen and (max-width: 600px) {
.block--mobile {
display: block;
@@ -256,11 +280,17 @@ label {
width: 1px;
height: 100%;
}
[class*='hint--']:after,
[class*='hint--']:before {
background-color: #000;
border-radius: 0 4px 0 0;
}
[class*='hint--']:after {
text-transform: none;
font-weight: normal;
letter-spacing: 0.5px;
font-size: 14px;
border-radius: 4px;
}
.line {
@@ -373,6 +403,7 @@ a > svg {
float: right;
display: flex;
align-items: center;
margin-inline-start: 2rem;
}
.btn {
@@ -382,7 +413,7 @@ a > svg {
font-size: inherit;
background: transparent;
border: 3px solid var(--color-button);
border-radius: 5px;
border-radius: 2rem;
padding: 9px 15px;
cursor: pointer;
letter-spacing: 0.2px;
@@ -394,15 +425,22 @@ a > svg {
}
.btn--primary {
background: var(--color-button)
linear-gradient(180deg, rgba(0, 0, 0, 0.15) 0px, transparent);
--black-mix: 70%;
background: linear-gradient(
180deg,
var(--color-button),
color-mix(in lch, var(--color-button), black)
);
color: black;
font-weight: 600;
border: 1px solid
color-mix(in lch, var(--color-button), black var(--black-mix));
box-shadow: inset 0 1px 0px 0 rgba(255, 255, 255, 0.15);
}
.btn--big {
padding: 15px 30px;
border-radius: 3px;
border-radius: 0.5rem;
}
.btn-icon {
@@ -410,11 +448,25 @@ a > svg {
align-items: center;
}
.btn:hover {
text-decoration: none;
box-shadow: 0 5px 5px 0 rgba(0, 0, 0, 0.25);
.btn--small {
padding: 0.2rem 0.5rem;
text-transform: uppercase;
/* border-radius: 3px; */
font-size: 0.8rem;
}
.btn:hover {
--black-mix: 90%;
text-decoration: none;
box-shadow: rgba(0, 0, 0, 0.1) 0px 20px 25px -5px,
rgba(0, 0, 0, 0.04) 0px 10px 10px -5px;
}
.btn:disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
*:focus {
outline-width: 3px;
outline-color: var(--color-button);
@@ -431,9 +483,34 @@ a > svg {
}
.btn--big > svg {
width: 25px;
height: 25px;
margin-right: 12px;
width: 2rem;
height: 2rem;
margin-right: 1rem;
}
@property --angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.btn--pro {
background: linear-gradient(
var(--angle),
var(--color-pro-1),
var(--color-pro-2)
);
color: #222;
border: 0;
font-weight: 700;
animation: gradient_move 3s linear infinite;
}
@keyframes gradient_move {
from {
--angle: 0deg;
}
to {
--angle: 360deg;
}
}
.btn-loader {
@@ -472,15 +549,13 @@ a > svg {
display: flex;
flex-direction: column;
overflow: hidden;
transition: 0.1s ease;
transition: 0.25s ease;
will-change: filter, transform;
}
body:not(.light-version).overlay-visible .main-container {
transition-duration: 0.5s;
transform: scale(0.98);
/* transition-delay: 0.4s; */
filter: blur(2px);
}
.content-wrap {
@@ -560,7 +635,7 @@ body:not(.light-version).overlay-visible .main-container {
}
.is-detached-mode.layout-2 .code-side {
height: auto !important;
height: 100% !important;
}
.code-wrap {
@@ -571,9 +646,7 @@ body:not(.light-version).overlay-visible .main-container {
overflow: hidden;
position: relative;
background: var(--color-bg);
transition:
height 0.3s ease,
width 0.3s ease;
transition: height 0.3s ease, width 0.3s ease;
will-change: height;
}
@@ -762,7 +835,7 @@ body > #demo-frame {
display: flex;
justify-content: space-between;
/* Because .console is 6 */
z-index: 6;
z-index: var(--zindex-footer);
}
.main-header {
@@ -774,6 +847,8 @@ body > #demo-frame {
.btn--dark,
.main-header__btn-wrap > button {
--clr-1: hsl(0, 0%, 25%);
--clr-2: hsl(0, 0%, 13%);
box-sizing: content-box;
/* text-transform: uppercase; */
/* font-size: 0.875rem; */
@@ -787,7 +862,8 @@ body > #demo-frame {
margin-left: 10px;
padding: 3px 8px;
border: 1px solid rgba(0, 0, 0, 0.9);
background: linear-gradient(180deg, hsl(0, 0%, 25%) 0, hsl(0, 0%, 13%) 100%);
border-radius: 5px;
background: linear-gradient(180deg, var(--clr-1) 0, var(--clr-2) 100%);
box-shadow: inset 0 1px 0px 0 rgba(255, 255, 255, 0.15);
}
@@ -805,13 +881,20 @@ body > #demo-frame {
}
.btn--dark:hover {
background: #9297b3;
/* --clr-1: #6844ad; */
--clr-1: hsl(53.35deg 100% 50%);
--clr-2: hsl(38.96deg 100% 50%);
color: #111;
/* border-color: rgba(146, 151, 179, 0.5); */
}
.btn--dark:hover > svg {
fill: #111;
}
.btn--dark.btn--active {
background: linear-gradient(0deg, hsl(0, 0%, 25%) 0, hsl(0, 0%, 13%) 100%);
box-shadow: inset 0 -1px 0px 0 rgba(255, 255, 255, 0.15);
}
.btn--chromeless {
box-shadow: none;
background: transparent;
@@ -938,6 +1021,29 @@ body > #demo-frame {
/* border-radius: 4px; */
}
.dialog__close-btn {
display: flex;
position: absolute;
border: none;
background: var(--color-close-btn);
background: linear-gradient(
to bottom,
var(--color-close-btn),
color-mix(in lch, var(--color-close-btn), black)
);
color: white;
border-radius: 0.3rem;
padding: 0.4rem 0.5rem;
}
.dialog__close-btn > svg {
width: 1.2rem;
aspect-ratio: 1;
}
.dialog__close-btn:hover {
--color-close-btn: var(--color-btn-hover-1);
color: #111;
}
.modal {
position: fixed;
top: 0;
@@ -949,13 +1055,23 @@ body > #demo-frame {
display: flex;
align-items: baseline;
justify-content: center;
z-index: 2000;
z-index: var(--zindex-modal);
visibility: hidden;
/* background-color: rgba(102, 51, 153, 0.7); */
/* background-color: rgba(0, 0, 0, 0.7); */
/* To prevent scroll repaint inside modal */
z-index: var(--zindex-modal-overlay);
/* opacity: 0; */
will-change: opacity;
background: var(--color-overlay);
backdrop-filter: blur(5px) grayscale(1);
transition: opacity var(--duration-modal-overlay-show);
will-change: transform;
}
.modal--no-overlay {
background: none;
backdrop-filter: none;
}
@keyframes anim-modal-overlay {
to {
@@ -964,32 +1080,26 @@ body > #demo-frame {
}
}
.modal__close-btn {
position: absolute;
right: 1rem;
top: 1rem;
text-transform: uppercase;
font-weight: 700;
font-size: 0.8rem;
opacity: 0.8;
transition: 0.25s ease;
border: 1px solid black;
border-radius: 2px;
padding: 0.2rem 0.5rem;
}
.modal__close-btn > svg {
fill: black;
width: 30px;
height: 30px;
}
.modal__close-btn:hover {
opacity: 0.7;
--color-close-btn: var(--color-btn-hover-1);
color: #111;
}
.modal__close-btn:hover > svg {
fill: #111;
}
.modal__close-btn {
right: 0rem;
bottom: calc(100% + 0.2rem);
transition: 0.25s ease;
}
.modal__content {
background: var(--color-popup);
/* fix me */
background: linear-gradient(45deg, #2d063cad, #3a2b63);
box-shadow: inset 1px -1px 0 0 #ffffff17, 0 20px 31px 0 #0000008a;
color: var(--color-text);
position: relative;
border-radius: 5px;
@@ -998,9 +1108,9 @@ body > #demo-frame {
font-size: 1.1em;
line-height: 1.4;
max-width: 85vw;
margin: 2rem auto;
margin: 4rem auto 2rem;
box-sizing: border-box;
overflow-y: auto;
/* overflow-y: auto; */
pointer-events: auto;
transform: scale(0.98);
animation: anim-modal var(--duration-modal-show) cubic-bezier(0.4, 0, 0.2, 1)
@@ -1029,6 +1139,7 @@ body > #demo-frame {
/* transition-duration: 0.3s; */
/* transform: translateY(0px) scale(1); */
/* opacity: 1; */
backdrop-filter: blur(3px);
}
.modal-overlay {
@@ -1038,10 +1149,11 @@ body > #demo-frame {
visibility: hidden;
top: 0;
left: 0;
z-index: 5;
z-index: var(--zindex-modal-overlay);
opacity: 0;
will-change: opacity;
background: rgba(0, 0, 0, 0.5);
/* background: var(--color-overlay); */
backdrop-filter: blur(5px) grayscale(1);
transition: opacity var(--duration-modal-overlay-show);
}
@@ -1071,11 +1183,13 @@ body > #demo-frame {
right: 0;
top: 0;
bottom: 0;
width: 450px;
width: 35vw;
min-width: 40ch;
max-width: 60ch;
padding: 20px 30px;
z-index: 6;
visibility: hidden; /* prevents tabbing */
background-color: var(--color-popup);
background: var(--color-popup);
transition: 0.3s cubic-bezier(1, 0.13, 0.21, 0.87);
transition-property: transform;
will-change: transform;
@@ -1096,23 +1210,16 @@ body > #demo-frame {
}
.saved-items-pane__close-btn {
position: absolute;
left: -18px;
top: 24px;
opacity: 0;
visibility: hidden;
border-radius: 50%;
padding: 10px 14px;
background: crimson;
color: white;
border: 0;
transform: scale(0);
will-change: transform, opacity;
transition: 0.3s ease;
transition-property: transform, opacity;
transition-delay: 0;
}
.saved-items-pane.is-open .saved-items-pane__close-btn {
opacity: 1;
transition-delay: 0.4s;
@@ -1124,8 +1231,6 @@ body > #demo-frame {
padding: 20px;
background-color: rgba(255, 255, 255, 0.06);
position: relative;
/*border: 1px solid rgba(255,255,255,0.1);*/
margin: 20px 0;
display: block;
border-radius: 4px;
cursor: pointer;
@@ -1135,6 +1240,10 @@ body > #demo-frame {
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
/* animation: slide-left 0.35s ease forwards; */
}
.saved-item-tile + .saved-item-tile {
margin-top: 1rem;
}
.saved-item-tile--inline {
display: inline-block;
margin-left: 10px;
@@ -1212,6 +1321,8 @@ body > #demo-frame {
.saved-item-tile__btns {
position: absolute;
top: 6px;
display: flex;
align-items: center;
z-index: 1;
right: 8px;
opacity: 0;
@@ -1230,14 +1341,21 @@ body > #demo-frame {
}
.saved-item-tile__btn {
display: inline-flex;
padding: 7px 10px;
color: white;
border: 0;
border-radius: 20px;
border-radius: 1in;
font-size: 0.8rem;
font-weight: 700;
margin-left: 2px;
background: rgba(255, 255, 255, 0.1);
text-transform: uppercase;
}
.saved-item-tile__btn > svg {
width: 1rem;
aspect-ratio: 1;
}
.saved-item-tile__btn:hover {
background: rgba(255, 255, 255, 0.8);
@@ -1260,7 +1378,9 @@ body > #demo-frame {
.saved-items-pane__container {
overflow-y: auto;
overflow-x: hidden;
max-height: calc(100vh - 90px);
max-height: calc(100vh - 130px);
margin-top: 1rem;
padding-inline: 1rem;
scroll-behavior: smooth;
}
@@ -1484,13 +1604,16 @@ body > #demo-frame {
}
.onboard-step {
margin: 15px;
padding: 20px 30px;
margin: 0.5rem;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
border-radius: 0.8rem;
box-shadow: 0 16px 22px rgba(0, 0, 0, 0.1);
flex: 1;
}
.onboard-step--full-width {
grid-column: 1 / -1;
}
.onboard-step__icon {
fill: rgba(255, 255, 255, 0.3);
@@ -1644,38 +1767,22 @@ body > #demo-frame {
display: inline-block;
}
.beta-tag {
border-radius: 4px;
text-transform: uppercase;
background: #c68955;
color: white;
letter-spacing: 0.6px;
padding: 2px 5px;
font-size: 0.9em;
}
.is-extension .web-maker-with-tag:after {
display: none;
}
.social-login-btn--github {
color: white;
background: #656b6f;
border-color: #656b6f;
}
.social-login-btn--facebook {
color: white;
background: #4e62c0;
border-color: #4e62c0;
}
.social-login-btn--google {
.social-login-btn {
background: white;
border: 2px solid currentColor;
color: black;
}
.social-login-btn--github {
}
.social-login-btn--google {
}
body.is-logged-in .hide-on-login,
body:not(.is-logged-in) .hide-on-logout,
body:not(.is-extension) .show-when-extension,
@@ -2008,6 +2115,300 @@ while the theme CSS file is loading */
justify-content: center;
}
.asset-manager__upload-box {
padding: 1rem 2rem;
border: 3px dashed rgb(255 255 255 / 10%);
border-radius: 1rem;
text-align: center;
margin-bottom: 1rem;
transition: 0.3s ease;
position: relative;
}
@keyframes move {
0% {
background-position: 0 0;
}
100% {
background-position: 50px 50px;
}
}
.asset-manager__progress-bar {
position: absolute;
inset: 1rem;
/* height: 2rem; */
background-image: linear-gradient(
-45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
z-index: 1;
background-size: 50px 50px;
animation: move 2s linear infinite;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
border-top-left-radius: 20px;
border-bottom-left-radius: 20px;
overflow: hidden;
}
.asset-manager__file {
/* list-style: none; */
/* padding: 0; */
/* margin: 0; */
display: flex;
background: none;
gap: 0.5rem;
padding: 0.2rem;
border-radius: 0.4rem;
color: inherit;
border: 0;
transition: 0.3s ease;
/* align-items: center; */
}
.asset-manager__file:hover {
background: rgb(255 255 255 / 5%);
}
.asset-manager__file--grid {
display: flex;
flex-direction: column;
/* align-items: flex-start; */
}
.asset-manager__file-container {
padding: 0;
gap: 0.3rem;
}
.asset-manager__file-container--grid {
display: grid;
grid-template-columns: repeat(7, minmax(90px, 1fr));
max-width: 50rem;
}
.asset-manager__file {
display: flex;
gap: 0.5rem;
position: relative;
}
.asset-manager__file-name {
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.asset-manager__file-image {
height: 1.5rem;
aspect-ratio: 1;
border-radius: 0.3rem;
object-fit: contain;
}
.asset-manager__file--grid .asset-manager__file-image {
width: 100%;
height: auto;
}
.asset-manager__file-ext {
text-transform: uppercase;
position: absolute;
bottom: 0.5rem;
left: 1.4rem;
letter-spacing: -1px;
font-weight: 800;
display: none;
}
.asset-manager__file--grid .asset-manager__file-ext {
display: block;
}
.asset-manager__file-actions {
position: absolute;
bottom: 0;
right: 0;
opacity: 0;
padding: 0.5rem 0;
visibility: hidden;
transform: translateY(-0.5rem);
transition: 0.3s ease;
background-color: rgb(255 255 255 / 15%);
backdrop-filter: blur(5px);
}
.asset-manager__file--grid .asset-manager__file-actions {
left: 0;
bottom: 1.5em;
}
.asset-manager__file:hover .asset-manager__file-actions {
transform: translateY(0rem);
opacity: 1;
visibility: visible;
}
.stack {
display: flex;
align-items: center;
}
.stack > * {
margin: 0;
}
.badge {
text-transform: uppercase;
display: inline-block;
padding: 0.1rem 0.3rem;
background: rgb(255 255 255 / 40%);
border-radius: 1rem;
font-size: 0.75rem;
color: #222;
font-weight: 800;
line-height: 1;
}
.beta-tag {
background: #ee9d59;
}
.pro-badge {
display: inline-block;
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
color: #222;
box-shadow: inset 2px 2px 3px rgba(0, 0, 0, 0.3);
}
.templates-container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.templates-container .saved-item-tile {
margin: 0;
}
.pro-modal {
background: url('/assets/pro-panda-flying.png') no-repeat;
background-position: bottom right;
}
.plan-card {
background: rgb(255 255 255 / 10%);
border-radius: 0.4rem;
padding: 1rem;
--shadow-color: 253deg 47% 15%;
--shadow-elevation-low: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.34),
0.4px 0.8px 1px -1.2px hsl(var(--shadow-color) / 0.34),
1px 2px 2.5px -2.5px hsl(var(--shadow-color) / 0.34);
--shadow-elevation-medium: 0.3px 0.5px 0.7px hsl(var(--shadow-color) / 0.36),
0.8px 1.6px 2px -0.8px hsl(var(--shadow-color) / 0.36),
2.1px 4.1px 5.2px -1.7px hsl(var(--shadow-color) / 0.36),
5px 10px 12.6px -2.5px hsl(var(--shadow-color) / 0.36);
box-shadow: var(--shadow-elevation-low);
}
.profile-modal__avatar-img.is-pro {
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
padding: 0.2rem;
animation: avatar-rotate 2s forwards;
}
@keyframes avatar-rotate {
0% {
transform: rotate(10deg);
}
100% {
transform: rotate(0deg);
}
}
.profile-modal__name.is-pro {
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
color: transparent;
background-clip: text;
}
.profile-modal__panda {
animation: slide-up 0.3s forwards;
}
@keyframes slide-up {
0% {
opacity: 0;
transform: translateX(-1.5rem);
}
100% {
opacity: 1;
transform: rotate(0deg);
}
}
/* .PANEL */
.panel {
--panel-bg: rgb(255 255 255 / 5%);
position: relative;
background: var(--panel-bg);
box-shadow: var(--panel-shadow);
border-radius: 1rem;
/* backdrop-filter: blur(20px); */
overflow: hidden;
}
.panelOnlyBorder {
background: none;
box-shadow: none;
backdrop-filter: none;
border: 2px solid var(--clr-border-1);
}
.panelGlowing {
box-shadow: var(--glow-shadow);
}
.panelTopFocus {
background: radial-gradient(
82.25% 100% at 50% 0%,
rgba(var(--rgb-gray-1), 0.75) 37.28%,
rgba(var(--rgb-gray-0), 0) 100%
);
box-shadow: 0 0 30px rgba(var(--rgb-brand), 0),
0px 20px 50px rgba(0, 0, 0, 0.1), inset 0px 1px 3px rgba(255, 255, 255, 0.1);
}
.skeleton {
display: inline-block;
height: 0.5em;
background-color: currentColor;
border-radius: 999px;
animation: 2s linear infinite skeleton;
transform-origin: left;
}
@keyframes skeleton {
20% {
transform-origin: left;
opacity: 1;
transform: scaleX(1.5);
opacity: 0.5;
}
20.1% {
transform-origin: right;
transform: scaleX(1.5) translateX(33%);
}
50% {
transform-origin: right;
transform: scaleX(1) translateX(50%);
opacity: 1;
}
70% {
transform-origin: right;
transform: scaleX(1.5) translateX(33%);
}
70.1% {
transform-origin: left;
transform: scaleX(1.5);
}
90% {
transform-origin: left;
transform: scaleX(1);
opacity: 0.5;
}
/* 100% {
transform-origin: right;
transform: scaleX(1.5) translateX(33%);
opacity: 1;
} */
}
@media screen and (max-width: 600px) {
body {
font-size: 70%;

View File

@@ -15,10 +15,25 @@ export default [
title: 'Preact',
img: 'assets/preact-logo.svg'
},
{
id: 'tailwind2',
title: 'Tailwind CSS 2',
img: 'assets/tailwind-logo.svg'
},
{
id: 'tailwind',
title: 'Tailwind CSS 3',
img: 'assets/tailwind-logo.svg'
},
{
id: 'kontra-game-engine',
title: 'Kontra Game Engine',
img: 'assets/html5-logo.svg',
isFileModeSupported: true
},
{
id: 'kaboom',
title: 'Kaboom',
img: 'assets/kaboom-logo.png'
}
];

View File

@@ -0,0 +1,18 @@
{
"title": "Kaboom game engine template",
"content": "",
"html": "",
"css": "",
"js": "// Responding to gravity & jumping\n\n// Start kaboom\nkaboom()\n\n// Load assets\nloadSprite(\"bean\", \"https://kaboomjs.com/sprites/bean.png\")\n\n// Set the gravity acceleration (pixels per second)\nsetGravity(1600)\n\n// Add player game object\nconst player = add([\n\tsprite(\"bean\"),\n\tpos(center()),\n\tarea(),\n\t// body() component gives the ability to respond to gravity\n\tbody(),\n])\n\nonKeyPress(\"space\", () => {\n\t// .isGrounded() is provided by body()\n\tif (player.isGrounded()) {\n\t\t// .jump() is provided by body()\n\t\tplayer.jump()\n\t}\n})\n\n// .onGround() is provided by body(). It registers an event that runs whenever player hits the ground.\nplayer.onGround(() => {\n\tdebug.log(\"ouch\")\n})\n\n// Accelerate falling when player holding down arrow key\nonKeyDown(\"down\", () => {\n\tif (!player.isGrounded()) {\n\t\tplayer.vel.y += dt() * 1200\n\t}\n})\n\n// Jump higher if space is held\nonKeyDown(\"space\", () => {\n\tif (!player.isGrounded() && player.vel.y < 0) {\n\t\tplayer.vel.y -= dt() * 600\n\t}\n})\n\n// Add a platform to hold the player\nadd([\n\trect(width(), 48),\n\toutline(4),\n\tarea(),\n\tpos(0, height() - 48),\n\t// Give objects a body() component if you don't want other solid objects pass through\n\tbody({ isStatic: true }),\n])\n\nadd([\n\ttext(\"Press space key\", { width: width() / 2 }),\n\tpos(12, 12),\n])\n\n// Check out https://kaboomjs.com#BodyComp for everything body() provides\n",
"externalLibs": {
"js": "\nhttps://unpkg.com/kaboom@3000/dist/kaboom.js",
"css": ""
},
"layoutMode": 1,
"htmlMode": "html",
"cssMode": "css",
"jsMode": "js",
"mainSizes": [50, 50],
"sizes": ["33px", "33px", 100]
}

View File

@@ -1,16 +1,16 @@
{
"title": "Preact template",
"externalLibs": {
"js": "\nhttps://cdnjs.cloudflare.com/ajax/libs/preact/8.2.9/preact.min.js",
"css": ""
},
"sizes": ["calc(30% - 3px)", "30px", "calc(70% - 3px)"],
"mainSizes": [68.1051, 31.6949],
"sizes": [30, "30px", 70],
"mainSizes": [60, 40],
"htmlMode": "html",
"cssMode": "css",
"jsMode": "es6",
"layoutMode": 1,
"js":
"\nconst { h, Component, render, createElement } = window.preact;\nconst React = {createElement}\n\nclass App extends Component {\n constructor() {\n super();\n this.message = 'World';\n }\n render() {\n return (<div>Hello {this.message}</div>)\n }\n}\nrender(<App/>, window.root)\n\n",
"externalLibs": {
"js": "https://unpkg.com/preact@10.21.0/dist/preact.min.umd.js\nhttps://unpkg.com/preact@10.21.0/hooks/dist/hooks.umd.js",
"css": ""
},
"js": "\nconst { h, render, createElement } = window.preact;\nconst { useState} = window.preactHooks;\nconst React = {createElement};\n\n\nfunction App() {\n const [count, setCount] = useState(0)\n return <div>\n <h1>Hello, I'm Preact!</h1>\n <p>{ count }</p>\n <button onClick={() => setCount(count+1)}>Click me!</button>\n </div>\n}\n\nrender(<App/>, document.body)\n\n\n",
"html": "<div id=\"root\"></div>\n"
}

View File

@@ -0,0 +1,17 @@
{
"title": "Tailwind CSS 3 template",
"html": "<div class=\"m-6 p-3 shadow-lg text-xl bg-purple-100 text-purple-800 rounded-md\">\n Hello world\n</div>",
"css": "",
"js": "",
"externalLibs": {
"js": "https://cdn.tailwindcss.com/3.4.3",
"css": ""
},
"layoutMode": 1,
"htmlMode": "html",
"cssMode": "css",
"jsMode": "js",
"updatedOn": 1713157902575,
"mainSizes": [50, 50],
"sizes": [51, 45, 4]
}

View File

@@ -0,0 +1,17 @@
{
"title": "Tailwind CSS 3 template",
"html": "<div class=\"m-6 p-3 shadow-lg text-xl bg-purple-100 text-purple-800 rounded-md\">\n Hello world\n</div>",
"css": "",
"js": "",
"externalLibs": {
"js": "",
"css": "https://cdn.jsdelivr.net/npm/tailwindcss/dist/tailwind.min.css"
},
"layoutMode": 1,
"htmlMode": "html",
"cssMode": "css",
"jsMode": "js",
"updatedOn": 1713157902575,
"mainSizes": [50, 50],
"sizes": [51, 45, 4]
}

View File

@@ -1,9 +1,9 @@
import { trackEvent } from './analytics';
import { computeHtml, computeCss, computeJs } from './computes';
import { modes, HtmlModes, CssModes, JsModes } from './codeModes';
import { deferred } from './deferred';
import { getExtensionFromFileName } from './fileUtils';
import confetti from 'canvas-confetti';
const esprima = require('esprima');
window.DEBUG = document.cookie.indexOf('wmdebug') > -1;
@@ -23,7 +23,7 @@ export const BASE_PATH =
window.DEBUG ||
process.env.NODE_ENV === 'development'
? '/'
: '/app';
: '/create';
/* eslint-enable no-process-env */
var alphaNum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
@@ -205,6 +205,41 @@ export function getHumanDate(timestamp) {
return retVal;
}
/**
* Convert any date-ish string/obj to human readable form -> Jul 02, 2021
* @param {string?object} date date to be formatted
* @returns string
*/
export function getHumanReadableDate(
date,
{ showTime = true, utc = false } = {}
) {
if (!date) return '';
let d = typeof date.toDate === 'function' ? date.toDate() : new Date(date);
if (utc) {
d = new Date(
Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours())
);
}
let options = {
year: 'numeric',
month: 'short',
day: 'numeric'
};
if (showTime) {
options = {
...options,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
};
}
const dateTimeString = d.toLocaleString(false, options);
return dateTimeString;
}
// create a one-time event
export function once(node, type, callback) {
// create event
@@ -585,3 +620,53 @@ if (window.IS_EXTENSION) {
} else {
document.body.classList.add('is-app');
}
export async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy text: ', err);
}
}
export function showConfetti(time = 4) {
var end = Date.now() + time * 1000;
(function frame() {
confetti({
particleCount: 1,
startVelocity: 0,
ticks: 100,
origin: {
x: Math.random(),
// since they fall down, start a bit higher than random
y: Math.random() - 0.2
},
colors: [
[
'#26ccff',
'#a25afd',
'#ff5e7e',
'#88ff5a',
'#fcff42',
'#ffa62d',
'#ff36ff'
][~~(Math.random() * 7)]
]
});
if (Date.now() < end) {
requestAnimationFrame(frame);
}
})();
}
/**
* Persists the firebase user with a subset of it's keys.
* @param {object} user User object from firebase
*/
export function persistAuthUserLocally(user) {
const keys = ['uid', 'displayName', 'photoURL', 'isPro', 'settings'];
const obj = {};
keys.map(key => (obj[key] = user[key]));
window.localStorage.setItem('user', JSON.stringify(obj));
}