').concat(this.tip,"")}},{key:"dispatchCustomEvent",value:function(t){var e=new CustomEvent(t,{bubbles:!0,cancelable:!0});e.relatedTarget=this,this.dispatchEvent(e),this.removeEventListener(t,this)}}],c=[{key:"observedAttributes",get:function(){return["type","label","tip","text","position"]}}],o&&e(n.prototype,o),c&&e(n,c),Object.defineProperty(n,"prototype",{writable:!1}),n;var n,o,c}();customElements.define("joomla-tip",s);
2 |
--------------------------------------------------------------------------------
/docs/_media/js/joomla-tip.js:
--------------------------------------------------------------------------------
1 | class TipElement extends HTMLElement {
2 | /* Attributes to monitor */
3 | static get observedAttributes() { return ['type', 'label', 'tip', 'text', 'position']; }
4 |
5 | get type() { return this.getAttribute('type'); }
6 |
7 | set type(value) { this.setAttribute('type', value); }
8 |
9 | get label() { return this.getAttribute('label'); }
10 |
11 | set label(value) { this.setAttribute('label', value); }
12 |
13 | get tip() { return this.getAttribute('tip'); }
14 |
15 | set tip(value) { this.setAttribute('tip', value); }
16 |
17 | get position() { return this.getAttribute('position'); }
18 |
19 | set position(value) { this.setAttribute('position', value); }
20 |
21 | get text() { return this.getAttribute('text'); }
22 |
23 | set text(value) { this.getAttribute('text', value); }
24 |
25 | /* Lifecycle, element appended to the DOM */
26 | connectedCallback() {
27 | if (!this.position || (this.position && ['top', 'bottom', 'left', 'right'].indexOf(this.position) === -1)) {
28 | this.position = 'top';
29 | }
30 |
31 | // create the html
32 | this.btnElement = document.createElement('button');
33 | this.spanElement = document.createElement('span');
34 |
35 | this.btnElement.setAttribute('aria-label', this.label ? this.label : 'more info');
36 | this.btnElement.innerHTML = this.text ? this.text : '';
37 | this.spanElement.setAttribute('role', 'status');
38 |
39 | // On click
40 | this.btnElement.addEventListener('click', this.showTip.bind(this));
41 |
42 | this.append(this.btnElement);
43 | this.append(this.spanElement);
44 | }
45 |
46 | /* Lifecycle, element removed from the DOM */
47 | disconnectedCallback() {
48 | this.querySelector('button').removeEventListener('click', this.showTip, true);
49 | }
50 |
51 | showTip() {
52 | const self = this;
53 |
54 | // Close on outside click
55 | document.addEventListener('click', (e) => {
56 | if (this.btnElement !== e.target) {
57 | this.spanElement.innerHTML = '';
58 | self.removeEventListener('keydown', this);
59 | }
60 | });
61 |
62 | // Remove toggletip on ESC
63 | document.addEventListener('keydown', (e) => {
64 | if ((e.keyCode || e.which) === 9) {
65 | this.spanElement.innerHTML = '';
66 | self.removeEventListener('keydown', this);
67 | }
68 | });
69 |
70 | this.spanElement.innerHTML = `${this.tip} `;
71 | }
72 |
73 | /* Method to dispatch events */
74 | dispatchCustomEvent(eventName) {
75 | const OriginalCustomEvent = new CustomEvent(eventName, { bubbles: true, cancelable: true });
76 | OriginalCustomEvent.relatedTarget = this;
77 | this.dispatchEvent(OriginalCustomEvent);
78 | this.removeEventListener(eventName, this);
79 | }
80 | }
81 | customElements.define('joomla-tip', TipElement);
82 |
--------------------------------------------------------------------------------
/docs/_media/js/joomla-tip.min.js:
--------------------------------------------------------------------------------
1 | class t extends HTMLElement{static get observedAttributes(){return["type","label","tip","text","position"]}get type(){return this.getAttribute("type")}set type(t){this.setAttribute("type",t)}get label(){return this.getAttribute("label")}set label(t){this.setAttribute("label",t)}get tip(){return this.getAttribute("tip")}set tip(t){this.setAttribute("tip",t)}get position(){return this.getAttribute("position")}set position(t){this.setAttribute("position",t)}get text(){return this.getAttribute("text")}set text(t){this.getAttribute("text",t)}connectedCallback(){(!this.position||this.position&&-1===["top","bottom","left","right"].indexOf(this.position))&&(this.position="top"),this.btnElement=document.createElement("button"),this.spanElement=document.createElement("span"),this.btnElement.setAttribute("aria-label",this.label?this.label:"more info"),this.btnElement.innerHTML=this.text?this.text:"",this.spanElement.setAttribute("role","status"),this.btnElement.addEventListener("click",this.showTip.bind(this)),this.append(this.btnElement),this.append(this.spanElement)}disconnectedCallback(){this.querySelector("button").removeEventListener("click",this.showTip,!0)}showTip(){const t=this;document.addEventListener("click",(e=>{this.btnElement!==e.target&&(this.spanElement.innerHTML="",t.removeEventListener("keydown",this))})),document.addEventListener("keydown",(e=>{9===(e.keyCode||e.which)&&(this.spanElement.innerHTML="",t.removeEventListener("keydown",this))})),this.spanElement.innerHTML=`${this.tip} `}dispatchCustomEvent(t){const e=new CustomEvent(t,{bubbles:!0,cancelable:!0});e.relatedTarget=this,this.dispatchEvent(e),this.removeEventListener(t,this)}}customElements.define("joomla-tip",t);
2 |
--------------------------------------------------------------------------------
/docs/_sidebar.md:
--------------------------------------------------------------------------------
1 | - Getting started
2 | - [Quick start](/quickstart)
3 |
4 | - Components
5 | - [Alert](/alert)
6 | - [Collapse](/collapse)
7 | - [Dropdown](/dropdown)
8 | - [Modal](/modal)
9 | - [Tab](/tab)
10 | - [Tip](/tip)
11 |
12 | - Customization
13 | - [Theming](/styles)
14 |
--------------------------------------------------------------------------------
/docs/collapse.md:
--------------------------------------------------------------------------------
1 | # Collapse WIP
2 |
3 | In order to use the collapse custom element you need to import the element in the document's head:
4 | ```html
5 |
6 |
7 | ```
8 |
9 | The simplified version of the custom elements
10 | ```html
11 |
12 |
13 | Link with href
14 |
15 |
17 | Button with data-target
18 |
19 |
20 |
21 |
22 | Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Nihil anim keffiyeh helvetica,
23 | craft beer labore wes anderson cred nesciunt sapiente ea proident.
24 |
25 |
26 | ```
27 |
28 | ### Collapse demo:
29 |
30 |
31 |
32 |
33 | Link with href
34 |
35 |
37 | Button with data-target
38 |
39 |
40 |
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident.
41 |
42 |
--------------------------------------------------------------------------------
/docs/dropdown.md:
--------------------------------------------------------------------------------
1 | # Dropdown WIP
2 |
3 | In order to use the dropdown custom element you need to import the element in the document's head:
4 | ```html
5 |
6 |
7 | ```
8 |
9 | The simplified version of the custom elements
10 | ```html
11 |
20 |
21 |
22 |
Dropdown with text
23 |
24 |
25 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
26 |
27 |
28 | ```
29 |
30 | ### Dropdown demo:
31 |
32 |
33 |
41 |
42 |
Dropdown with text
43 |
44 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Joomla UI components
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | En
31 |
32 |
33 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/docs/modal.md:
--------------------------------------------------------------------------------
1 | # Modal WIP
2 |
3 | In order to use the modal custom element you need to import the element in the document's head:
4 | ```html
5 |
6 |
7 | ```
8 |
9 | The simplified version of the custom elements
10 | ```html
11 | Launch demo modal
12 |
13 |
14 |
15 |
18 |
19 | Close
20 | Save changes
21 |
22 |
23 | ```
24 |
25 | ### Modal demo:
26 |
27 | Launch demo modal
28 |
29 |
32 |
33 | Close
34 | Save changes
35 |
36 |
37 |
38 | Modal with iframe
39 |
40 |
43 |
44 | Close
45 | Save changes
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quick start
2 |
3 | It is recommended to install this repo locally so you can customize the css to match your app/theme/template colours. To do so you can use `npm`:
4 |
5 | ```bash
6 | $ npm i joomla-ui-custom-elements
7 | ```
8 |
9 | ## Build
10 |
11 | To build the elements just use `grunt`:
12 |
13 | ```bash
14 | $ grunt
15 | ```
16 | !> So editing the scss files to match your styles and then running `grunt` will produce the elements based on your styles.
17 |
18 | !> You can change the prefix of the element as well, for simplicity this document will refer to elements with the standard prefix `joomla`
19 |
20 | ## Using the elements
21 |
22 | Due to compatibility issues due to browsers support you need to have a polyfill, so your page will render as expected.
23 | The code for this is available from the Polyfills repo on GitHub. Click [here](https://github.com/webcomponents/polyfills)
24 | for full information:
25 | - A shim so ES5 code can run flawlessly into ES6 Browsers
26 | ```html
27 |
28 | ```
29 | - A polyfill so custom elements can be used in older Browsers
30 | ```html
31 |
32 | ```
33 |
34 | For each element that you want to use in your page you have to import the relevant CSS and JS file on your page, e.g:
35 | ```html
36 |
37 |
38 | ```
39 |
40 | ?> The polyfill needs to be inserted only once before the first element.
41 |
42 | ## Customize your elements
43 |
44 | Every element has its own `.scss` file and there is a global `variables.scss` that can be used to tweak the styling to your own needs. Once you've changed the variables re-run `grunt` to rebuild the elements.
45 |
46 |
47 | ## Custom elements are CSS framework agnostic
48 |
49 | Check these collections of custom elements running quite happily with the most popular CSS frameworks:
50 |
51 | ----
52 | Bootstrap
53 | -----
54 | Foundation
55 | -----
56 | UiKit
57 | -----
58 |
--------------------------------------------------------------------------------
/docs/styles.md:
--------------------------------------------------------------------------------
1 | # Customising the look and feel of the components
2 |
--------------------------------------------------------------------------------
/docs/sw.js:
--------------------------------------------------------------------------------
1 | /* ===========================================================
2 | * docsify sw.js
3 | * ===========================================================
4 | * Copyright 2016 @huxpro
5 | * Licensed under Apache 2.0
6 | * Register service worker.
7 | * ========================================================== */
8 |
9 | const RUNTIME = 'docsify'
10 | const HOSTNAME_WHITELIST = [
11 | self.location.hostname,
12 | 'fonts.gstatic.com',
13 | 'fonts.googleapis.com',
14 | 'unpkg.com'
15 | ]
16 |
17 | // The Util Function to hack URLs of intercepted requests
18 | const getFixedUrl = (req) => {
19 | var now = Date.now()
20 | var url = new URL(req.url)
21 |
22 | // 1. fixed http URL
23 | // Just keep syncing with location.protocol
24 | // fetch(httpURL) belongs to active mixed content.
25 | // And fetch(httpRequest) is not supported yet.
26 | url.protocol = self.location.protocol
27 |
28 | // 2. add query for caching-busting.
29 | // Github Pages served with Cache-Control: max-age=600
30 | // max-age on mutable content is error-prone, with SW life of bugs can even extend.
31 | // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string.
32 | // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190
33 | if (url.hostname === self.location.hostname) {
34 | url.search += (url.search ? '&' : '?') + 'cache-bust=' + now
35 | }
36 | return url.href
37 | }
38 |
39 | /**
40 | * @Lifecycle Activate
41 | * New one activated when old isnt being used.
42 | *
43 | * waitUntil(): activating ====> activated
44 | */
45 | self.addEventListener('activate', event => {
46 | event.waitUntil(self.clients.claim())
47 | })
48 |
49 | /**
50 | * @Functional Fetch
51 | * All network requests are being intercepted here.
52 | *
53 | * void respondWith(Promise r)
54 | */
55 | self.addEventListener('fetch', event => {
56 | // Skip some of cross-origin requests, like those for Google Analytics.
57 | if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) {
58 | // Stale-while-revalidate
59 | // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale
60 | // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1
61 | const cached = caches.match(event.request)
62 | const fixedUrl = getFixedUrl(event.request)
63 | const fetched = fetch(fixedUrl, { cache: 'no-store' })
64 | const fetchedCopy = fetched.then(resp => resp.clone())
65 |
66 | // Call respondWith() with whatever we get first.
67 | // If the fetch fails (e.g disconnected), wait for the cache.
68 | // If there’s nothing in cache, wait for the fetch.
69 | // If neither yields a response, return offline pages.
70 | event.respondWith(
71 | Promise.race([fetched.catch(_ => cached), cached])
72 | .then(resp => resp || fetched)
73 | .catch(_ => { /* eat any errors */ })
74 | )
75 |
76 | // Update the cache with the version we fetched (only for ok status)
77 | event.waitUntil(
78 | Promise.all([fetchedCopy, caches.open(RUNTIME)])
79 | .then(([response, cache]) => response.ok && cache.put(event.request, response))
80 | .catch(_ => { /* eat any errors */ })
81 | )
82 | }
83 | })
84 |
--------------------------------------------------------------------------------
/docs/tip.md:
--------------------------------------------------------------------------------
1 | # Tip WIP
2 |
3 | In order to use the tip custom element you need to import the element in the document's head:
4 | ```html
5 |
6 |
7 | ```
8 |
9 | The simplified version of the custom elements
10 | ```html
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | ```
23 |
24 | ### tip demo:
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/karma-ci.conf.js:
--------------------------------------------------------------------------------
1 | // Browsers to run on Sauce Labs
2 | const customLaunchers = {
3 | SL_chrome: {
4 | base: 'SauceLabs',
5 | browserName: 'chrome',
6 | platform: 'Windows 10',
7 | version: 'latest',
8 | },
9 | SL_firefox: {
10 | base: 'SauceLabs',
11 | browserName: 'firefox',
12 | version: 'latest',
13 | },
14 | SL_safari: {
15 | base: 'SauceLabs',
16 | browserName: 'safari',
17 | version: 'latest',
18 | },
19 | // SL_ie_11: {
20 | // base: 'SauceLabs',
21 | // browserName: 'internet explorer',
22 | // browserVersion: '11.285',
23 | // platformName: 'Windows 10',
24 | // },
25 | };
26 |
27 | module.exports = (config) => {
28 | if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) {
29 | console.log('Make sure the SAUCE_USERNAME and SAUCE_ACCESS_KEY environment variables are set.');
30 | process.exit(1);
31 | }
32 |
33 | config.set({
34 | // list of files / patterns to load in the browser
35 | files: [
36 | // polyfill
37 | // {pattern: 'node_modules/@webreflection/custom-elements-no-builtin/min.js', served: true, nocache: true },
38 | // modules
39 | {pattern: 'dist/js/joomla-alert.js', type: 'module' },
40 | // {pattern: 'dist/js/joomla-collapse.js', type: 'module' },
41 | // {pattern: 'dist/js/joomla-dropdown.js', type: 'module' },
42 | // {pattern: 'dist/js/joomla-modal.js', type: 'module' },
43 | {pattern: 'dist/js/joomla-tab.js', type: 'module' },
44 | // {pattern: 'dist/js//joomla-tip.js', type: 'module' },
45 | // ES5
46 | // {pattern: 'dist/js/joomla-alert-es5.js', nomodule: '' },
47 | // {pattern: 'dist/js/joomla-collapse-es5.js', nomodule: '' },
48 | // {pattern: 'dist/js/joomla-dropdown-es5.js', nomodule: '' },
49 | // {pattern: 'dist/js/joomla-modal-es5.js', nomodule: '' },
50 | // {pattern: 'dist/js/joomla-panels-es5.js', nomodule: '' },
51 | // {pattern: 'dist/js/joomla-tab-es5.js', nomodule: '' },
52 | // {pattern: 'dist/js/joomla-tip-es5.js', nomodule: '' },
53 |
54 | // CSS
55 | 'dist/css/joomla-alert.css',
56 | 'dist/css/joomla-tab.css',
57 |
58 | { pattern: './tests/**/*.js' },
59 | { pattern: './tests/**/*.html' },
60 | ],
61 |
62 | preprocessors: {
63 | // 'packags/src/**/js/*.js': ['coverage'],
64 | 'tests/**/*.html': ['html2js'],
65 | },
66 | plugins: [
67 | 'karma-sauce-launcher',
68 | 'karma-jasmine',
69 | 'karma-fixture',
70 | 'karma-html2js-preprocessor',
71 | ],
72 | // frameworks to use
73 | frameworks: ['jasmine', 'fixture'],
74 |
75 | // SauceLabs Configuration
76 | sauceLabs: {
77 | testName: 'Web App Unit Tests',
78 | build: `GITHUB #${process.env.GITHUB_RUN_ID} (${process.env.GITHUB_RUN_NUMBER})`,
79 | startConnect: false,
80 | tunnelIdentifier: `github-action-tunnel-custom-elements-${process.env.GITHUB_RUN_ID}`,
81 | },
82 |
83 | reporters: ['dots', 'saucelabs'],
84 |
85 | // base path that will be used to resolve all patterns (eg. files, exclude)
86 | basePath: process.cwd(),
87 | runnerPort: 9100,
88 | colors: true,
89 | port: 9876,
90 | autoWatch: false,
91 | logLevel: config.LOG_INFO,
92 | singleRun: true,
93 | concurrency: 1,
94 | captureTimeout: 120000,
95 | browserNoActivityTimeout: 60000,
96 | browserDisconnectTimeout: 20000,
97 | browserDisconnectTolerance: 1,
98 |
99 | customLaunchers: customLaunchers,
100 | browsers: Object.keys(customLaunchers),
101 | });
102 | };
103 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | module.exports = function (config) {
3 | config.set({
4 | // frameworks to use
5 | frameworks: ['jasmine', 'fixture'],
6 |
7 | // list of files / patterns to load in the browser
8 | files: [
9 | // polyfill
10 | // {pattern: 'node_modules/@webreflection/custom-elements-no-builtin/min.js', served: true, nocache: true },
11 | // modules
12 | {pattern: 'dist/js/joomla-alert.js', type: 'module' },
13 | // {pattern: 'dist/js/joomla-collapse.js', type: 'module' },
14 | // {pattern: 'dist/js/joomla-dropdown.js', type: 'module' },
15 | // {pattern: 'dist/js/joomla-modal.js', type: 'module' },
16 | // {pattern: 'dist/js/joomla-panels.js', type: 'module' },
17 | {pattern: 'dist/js/joomla-tab.js', type: 'module' },
18 | // {pattern: 'dist/js//joomla-tip.js', type: 'module' },
19 | // ES5
20 | // {pattern: 'dist/js/joomla-alert-es5.js', nomodule: '' },
21 | // {pattern: 'dist/js/joomla-collapse-es5.js', nomodule: '' },
22 | // {pattern: 'dist/js/joomla-dropdown-es5.js', nomodule: '' },
23 | // {pattern: 'dist/js/joomla-modal-es5.js', nomodule: '' },
24 | // {pattern: 'dist/js/joomla-panels-es5.js', nomodule: '' },
25 | // {pattern: 'dist/js/joomla-tab-es5.js', nomodule: '' },
26 | // {pattern: 'dist/js/joomla-tip-es5.js', nomodule: '' },
27 |
28 | 'dist/css/joomla-alert.css',
29 | 'dist/css/joomla-tab.css',
30 |
31 | { pattern: './tests/**/*.js' },
32 | { pattern: './tests/**/*.html' },
33 | ],
34 |
35 | // test results reporter to use
36 | reporters: ['progress'],
37 |
38 | plugins: [
39 | require('karma-chrome-launcher'),
40 | 'karma-jasmine',
41 | 'karma-fixture',
42 | 'karma-html2js-preprocessor',
43 | ],
44 |
45 | preprocessors: {
46 | // 'packags/src/**/js/*.js': ['coverage'],
47 | 'tests/**/*.html': ['html2js'],
48 | },
49 |
50 | // base path that will be used to resolve all patterns (eg. files, exclude)
51 | basePath: process.cwd(),
52 | // web server port
53 | port: 9876,
54 | // enable / disable colors in the output (reporters and logs)
55 | colors: true,
56 | // level of logging
57 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
58 | logLevel: config.LOG_INFO,
59 | // enable / disable watching file and executing tests whenever any file changes
60 | autoWatch: true,
61 | // Start these browsers, currently available:
62 | // - Chrome
63 | // - ChromeCanary
64 | // - Firefox
65 | // - Opera
66 | // - Safari (only Mac)
67 | // - PhantomJS
68 | // - IE (only Windows)
69 | browsers: ['ChromeHeadless'],
70 | // Continuous Integration mode
71 | // if true, Karma captures browsers, runs the tests and exits
72 | singleRun: false
73 | });
74 | };
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "joomla-ui-custom-elements",
3 | "version": "0.4.1",
4 | "description": "Joomla UI components as custom elements",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/joomla-projects/custom-elements.git"
8 | },
9 | "license": "GPL-2.0-or-later",
10 | "bugs": {
11 | "url": "https://github.com/joomla-projects/custom-elements/issues"
12 | },
13 | "homepage": "https://github.com/joomla-projects/custom-elements#readme",
14 | "main": "src/index.js",
15 | "keywords": [
16 | "Joomla",
17 | "web-components",
18 | "custom-elements",
19 | "vanilla-js",
20 | "javascript"
21 | ],
22 | "author": "Dimitrios Grammatikogiannis",
23 | "devDependencies": {
24 | "@babel/core": "7.25.2",
25 | "@babel/preset-env": "7.25.4",
26 | "@rollup/plugin-babel": "6.0.4",
27 | "@rollup/plugin-node-resolve": "15.3.0",
28 | "@rollup/plugin-terser": "0.4.4",
29 | "@webreflection/custom-elements-no-builtin": "0.3.0",
30 | "autoprefixer": "10.4.20",
31 | "cssnano": "7.0.6",
32 | "eslint": "8.57.1",
33 | "eslint-config-airbnb": "19.0.4",
34 | "eslint-config-airbnb-base": "15.0.0",
35 | "eslint-plugin-import": "2.30.0",
36 | "karma": "6.4.4",
37 | "karma-chrome-launcher": "3.2.0",
38 | "karma-fixture": "0.2.6",
39 | "karma-html2js-preprocessor": "1.1.0",
40 | "karma-jasmine": "5.1.0",
41 | "karma-sauce-launcher": "4.3.6",
42 | "postcss": "8.4.47",
43 | "postcss-scss": "^4.0.9",
44 | "rimraf": "6.0.1",
45 | "rollup": "4.22.4",
46 | "rollup-plugin-sass": "1.13.2",
47 | "rollup-plugin-scss": "4.0.0",
48 | "sass": "1.79.3",
49 | "stylelint": "16.9.0",
50 | "stylelint-config-standard": "36.0.1",
51 | "stylelint-order": "6.0.4",
52 | "stylelint-scss": "6.7.0"
53 | },
54 | "scripts": {
55 | "build": "rimraf dist && node ./node_modules/rollup/dist/bin/rollup -c rollup.config.mjs",
56 | "lint:js": "node ./node_modules/eslint/bin/eslint.js src",
57 | "lint:css": "stylelint --config .stylelintrc.json \"src/scss/**/*.scss\"",
58 | "test": "node node_modules/karma/bin/karma start --single-run --browsers ChromeHeadless karma.conf.js",
59 | "ci-test": "node node_modules/karma/bin/karma start karma-ci.conf.js --single-run",
60 | "browserlist:update": "npx browserslist@latest --update-db"
61 | },
62 | "overrides": {
63 | "webdriverio": "^7.19.5"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base",
5 | ":preserveSemverRanges",
6 | ":disableMajorUpdates"
7 | ],
8 | "versioning": "semver",
9 | "dependencyDashboard": true,
10 | "lockFileMaintenance": { "enabled": true },
11 | "rangeStrategy": "update-lockfile",
12 | "constraints": {
13 | "npm": "> 8.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import { nodeResolve } from '@rollup/plugin-node-resolve';
2 | import terser from "@rollup/plugin-terser";
3 | import { getBabelOutputPlugin } from '@rollup/plugin-babel';
4 | import sass from 'rollup-plugin-sass';
5 | import autoprefixer from 'autoprefixer';
6 | import cssnano from 'cssnano';
7 | import postcss from 'postcss';
8 | import { existsSync } from 'fs';
9 | import { mkdir, writeFile } from 'fs/promises';
10 | import { dirname } from 'path';
11 |
12 | const Elements = [
13 | 'alert',
14 | 'collapse',
15 | 'dropdown',
16 | 'modal',
17 | 'tab',
18 | 'tip',
19 | ];
20 |
21 | const buildSettings = async () => {
22 | const finalSettings = [];
23 |
24 | Elements.forEach((file) => {
25 | finalSettings.push({
26 | input: `src/js/${file}/${file}.js`,
27 | plugins: [nodeResolve()],
28 | output: [
29 | { file: `dist/js/joomla-${file}.js`, format: 'esm' },
30 | { file: `docs/_media/js/joomla-${file}.js`, format: 'esm' },
31 | { file: `dist/js/joomla-${file}.min.js`, format: 'esm', plugins: [terser()] },
32 | { file: `docs/_media/js/joomla-${file}.min.js`, format: 'esm', plugins: [terser()] },
33 | ],
34 | });
35 |
36 | finalSettings.push({
37 | input: `src/js/${file}/${file}.js`,
38 | plugins: [nodeResolve(), getBabelOutputPlugin({ presets: ['@babel/preset-env'] })],
39 | output: [
40 | { file: `dist/js/joomla-${file}-es5.js`, format: 'esm' },
41 | { file: `docs/_media/js/joomla-${file}-es5.js`, format: 'esm' },
42 | { file: `dist/js/joomla-${file}-es5.min.js`, format: 'esm', plugins: [getBabelOutputPlugin({ presets: ['@babel/preset-env'] }), terser()] },
43 | { file: `docs/_media/js/joomla-${file}-es5.min.js`, format: 'esm', plugins: [getBabelOutputPlugin({ presets: ['@babel/preset-env'] }), terser()] },
44 | ],
45 | });
46 | });
47 |
48 | Elements.forEach((scssFile) => {
49 | finalSettings.push({
50 | input: `src/scss/${scssFile}/${scssFile}.scss`,
51 | plugins: [
52 | sass({
53 | output: false,
54 | processor: css => postcss([autoprefixer])
55 | .process(css)
56 | .then(async (result) => {
57 | const path1 = `dist/css/joomla-${scssFile}.css`;
58 | const path2 = `docs/_media/css/joomla-${scssFile}.css`;
59 | if (!existsSync(dirname(path1))) {
60 | await mkdir(dirname(path1), {recursive: true})
61 | }
62 | await writeFile(path1, result.css, {encoding: 'utf8'});
63 | if (!existsSync(dirname(path2))) {
64 | await mkdir(dirname(path2), {recursive: true})
65 | }
66 | await writeFile(path2, result.css, {encoding: 'utf8'});
67 |
68 | postcss([autoprefixer, cssnano])
69 | .process(result.css)
70 | .then(async (result) => {
71 | const path1 = `dist/css/joomla-${scssFile}.min.css`;
72 | const path2 = `docs/_media/css/joomla-${scssFile}.min.css`;
73 | if (!existsSync(dirname(path1))) {
74 | await mkdir(dirname(path1), {recursive: true})
75 | }
76 | await writeFile(path1, result.css, {encoding: 'utf8'});
77 | if (!existsSync(dirname(path2))) {
78 | await mkdir(dirname(path2), {recursive: true})
79 | }
80 | await writeFile(path2, result.css, {encoding: 'utf8'});
81 |
82 | });
83 | })
84 | }),
85 | ],
86 | });
87 | })
88 |
89 | return finalSettings;
90 | };
91 |
92 | export default buildSettings();
93 |
--------------------------------------------------------------------------------
/src/js/collapse/collapse.js:
--------------------------------------------------------------------------------
1 | customElements.define('joomla-collapse', class extends HTMLElement {
2 | static get observedAttributes() {
3 | return ['state'];
4 | }
5 |
6 | get state() { return this.getAttribute('state'); }
7 |
8 | set state(value) { this.setAttribute('state', value); }
9 |
10 | connectedCallback() {
11 | const self = this;
12 | // id is required
13 | if (!this.id) return;
14 |
15 | const linked = [].slice.call(document.querySelectorAll(`[href="#${this.id}"],[data-target="#${this.id}"]`));
16 |
17 | linked.forEach((element) => {
18 | if (!self.state || (self.state && self.state === 'closed')) {
19 | self.state = 'closed';
20 | element.setAttribute('aria-expanded', 'false');
21 | element.setAttribute('aria-controls', self.id);
22 | } else {
23 | element.setAttribute('aria-expanded', 'true');
24 | element.setAttribute('aria-controls', self.id);
25 | }
26 |
27 | element.addEventListener('click', (event) => {
28 | let colId = '';
29 | if (!event.target.hasAttribute('data-target')) colId = event.target.getAttribute('href').replace('#', '');
30 | if (!event.target.hasAttribute('href')) colId = event.target.getAttribute('data-target').replace('#', '');
31 | event.preventDefault();
32 | event.stopPropagation();
33 | document.getElementById(colId).toggle();
34 | });
35 | });
36 | }
37 |
38 | disconnectedCallback() {
39 | let linked = document.querySelector(`[href="#${this.id}"]`);
40 | if (!linked) linked = document.querySelector(`[data-target="#${this.id}"]`);
41 | if (linked) {
42 | linked.removeEventListener('click', this);
43 | }
44 | }
45 |
46 | attributeChangedCallback(attr, oldValue, newValue) {
47 | const linked = document.querySelector(`[href="#${this.id}"]`);
48 | switch (attr) {
49 | case 'state':
50 | if (newValue === 'closed') {
51 | linked.setAttribute('aria-expanded', 'false');
52 | } else if (newValue === 'open') {
53 | linked.setAttribute('aria-expanded', 'true');
54 | }
55 | break;
56 | default:
57 | break;
58 | }
59 | }
60 |
61 | toggle() {
62 | let linked = document.querySelector(`[href="#${this.id}"]`);
63 | if (!linked) linked = document.querySelector(`[data-target="#${this.id}"]`);
64 | if (this.state === 'closed') {
65 | this.state = 'open';
66 | linked.setAttribute('aria-expanded', 'true');
67 | } else {
68 | this.state = 'closed';
69 | linked.setAttribute('aria-expanded', 'false');
70 | }
71 | }
72 | });
73 |
--------------------------------------------------------------------------------
/src/js/dropdown/dropdown.js:
--------------------------------------------------------------------------------
1 | class JoomlaDropdownElement extends HTMLElement {
2 | /* Attributes to monitor */
3 | static get observedAttributes() {
4 | return ['for'];
5 | }
6 |
7 | get for() { return this.getAttribute('for'); }
8 | set for(value) { this.setAttribute('for', value); }
9 |
10 | connectedCallback() {
11 | this.setAttribute('aria-labelledby', this.for.substring(1));
12 | const button = document.querySelector(this.for);
13 | const innerLinks = this.querySelectorAll('a');
14 |
15 | if (!button.id) {
16 | return;
17 | }
18 | // var children = [].slice.call( menu[getElementsByTagName]('*'));
19 | // this.classList.add('dropdown');
20 |
21 | button.setAttribute('aria-haspopup', true);
22 | button.setAttribute('aria-expanded', false);
23 |
24 | button.addEventListener('click', (event) => {
25 | if (this.hasAttribute('expanded')) {
26 | this.removeAttribute('expanded');
27 | event.target.setAttribute('aria-expanded', false);
28 | } else {
29 | this.setAttribute('expanded', '');
30 | event.target.setAttribute('aria-expanded', true);
31 | }
32 |
33 | document.addEventListener('click', (evt) => {
34 | if (evt.target !== button) {
35 | if (!this.findAncestor(evt.target, 'joomla-dropdown')) {
36 | this.close();
37 | }
38 | }
39 | });
40 |
41 | innerLinks.forEach((innerLink) => {
42 | innerLink.addEventListener('click', () => {
43 | this.close();
44 | });
45 | });
46 | });
47 | }
48 |
49 | /*eslint-disable */
50 | /* Method to dispatch events */
51 | dispatchCustomEvent(eventName) {
52 | const OriginalCustomEvent = new CustomEvent(eventName);
53 | OriginalCustomEvent.relatedTarget = this;
54 | this.dispatchEvent(OriginalCustomEvent);
55 | this.removeEventListener(eventName, this);
56 | }
57 |
58 | adoptedCallback(oldDocument, newDocument) { }
59 |
60 |
61 | attributeChangedCallback(attr, oldValue, newValue) {
62 | switch (attr) {
63 | // case 'name':
64 | // console.log(newValue);
65 | // break;
66 | }
67 | }
68 | /* eslint-enable */
69 |
70 | close() {
71 | const button = document.querySelector(`#${this.getAttribute('aria-labelledby')}`);
72 | this.removeAttribute('expanded');
73 | button.setAttribute('aria-expanded', false);
74 | }
75 |
76 | /* eslint-disable */
77 | findAncestor(el, tagName) {
78 | while ((el = el.parentElement) && el.nodeName.toLowerCase() !== tagName);
79 | return el;
80 | }
81 | /* eslint-enable */
82 | }
83 |
84 | customElements.define('joomla-dropdown', JoomlaDropdownElement);
85 |
--------------------------------------------------------------------------------
/src/js/tab/tab-element.js:
--------------------------------------------------------------------------------
1 | export class TabElement extends HTMLElement {}
2 |
--------------------------------------------------------------------------------
/src/js/tip/tip.js:
--------------------------------------------------------------------------------
1 | class TipElement extends HTMLElement {
2 | /* Attributes to monitor */
3 | static get observedAttributes() { return ['type', 'label', 'tip', 'text', 'position']; }
4 |
5 | get type() { return this.getAttribute('type'); }
6 |
7 | set type(value) { this.setAttribute('type', value); }
8 |
9 | get label() { return this.getAttribute('label'); }
10 |
11 | set label(value) { this.setAttribute('label', value); }
12 |
13 | get tip() { return this.getAttribute('tip'); }
14 |
15 | set tip(value) { this.setAttribute('tip', value); }
16 |
17 | get position() { return this.getAttribute('position'); }
18 |
19 | set position(value) { this.setAttribute('position', value); }
20 |
21 | get text() { return this.getAttribute('text'); }
22 |
23 | set text(value) { this.getAttribute('text', value); }
24 |
25 | /* Lifecycle, element appended to the DOM */
26 | connectedCallback() {
27 | if (!this.position || (this.position && ['top', 'bottom', 'left', 'right'].indexOf(this.position) === -1)) {
28 | this.position = 'top';
29 | }
30 |
31 | // create the html
32 | this.btnElement = document.createElement('button');
33 | this.spanElement = document.createElement('span');
34 |
35 | this.btnElement.setAttribute('aria-label', this.label ? this.label : 'more info');
36 | this.btnElement.innerHTML = this.text ? this.text : '';
37 | this.spanElement.setAttribute('role', 'status');
38 |
39 | // On click
40 | this.btnElement.addEventListener('click', this.showTip.bind(this));
41 |
42 | this.append(this.btnElement);
43 | this.append(this.spanElement);
44 | }
45 |
46 | /* Lifecycle, element removed from the DOM */
47 | disconnectedCallback() {
48 | this.querySelector('button').removeEventListener('click', this.showTip, true);
49 | }
50 |
51 | showTip() {
52 | const self = this;
53 |
54 | // Close on outside click
55 | document.addEventListener('click', (e) => {
56 | if (this.btnElement !== e.target) {
57 | this.spanElement.innerHTML = '';
58 | self.removeEventListener('keydown', this);
59 | }
60 | });
61 |
62 | // Remove toggletip on ESC
63 | document.addEventListener('keydown', (e) => {
64 | if ((e.keyCode || e.which) === 9) {
65 | this.spanElement.innerHTML = '';
66 | self.removeEventListener('keydown', this);
67 | }
68 | });
69 |
70 | this.spanElement.innerHTML = `${this.tip} `;
71 | }
72 |
73 | /* Method to dispatch events */
74 | dispatchCustomEvent(eventName) {
75 | const OriginalCustomEvent = new CustomEvent(eventName, { bubbles: true, cancelable: true });
76 | OriginalCustomEvent.relatedTarget = this;
77 | this.dispatchEvent(OriginalCustomEvent);
78 | this.removeEventListener(eventName, this);
79 | }
80 | }
81 | customElements.define('joomla-tip', TipElement);
82 |
--------------------------------------------------------------------------------
/src/scss/_variables.scss:
--------------------------------------------------------------------------------
1 | //
2 | // Variables
3 | //
4 |
5 | // Color system
6 | $white: #fff;
7 | $gray-100: #f8f9fa;
8 | $gray-200: #e9ecef;
9 | $gray-300: #dee2e6;
10 | $gray-400: #ced4da;
11 | $gray-500: #adb5bd;
12 | $gray-600: #6c757d;
13 | $gray-700: #495057;
14 | $gray-800: #343a40;
15 | $gray-900: #212529;
16 | $black: #000;
17 |
18 | $blue: #006898;
19 | $indigo: #6610f2;
20 | $purple: #6f42c1;
21 | $pink: #e83e8c;
22 | $red: #d9534f;
23 | $orange: #fd7e14;
24 | $yellow: #f0ad4e;
25 | $green: #438243;
26 | $teal: #20c997;
27 | $cyan: #17a2b8;
28 |
29 | $colors: (
30 | blue: $blue,
31 | indigo: $indigo,
32 | purple: $purple,
33 | pink: $pink,
34 | red: $red,
35 | orange: $orange,
36 | yellow: $yellow,
37 | green: $green,
38 | teal: $teal,
39 | cyan: $cyan,
40 | white: $white,
41 | gray: $gray-600,
42 | gray-dark: $gray-800
43 | );
44 |
45 | $theme-colors: (
46 | primary: $blue,
47 | secondary: $gray-600,
48 | success: $green,
49 | info: $cyan,
50 | warning: $yellow,
51 | danger: $red,
52 | light: $gray-100,
53 | dark: $gray-800
54 | );
55 |
56 | $alert-colors: (
57 | success: $green,
58 | info: $cyan,
59 | warning: $yellow,
60 | danger: $red
61 | );
62 |
63 | $theme-color-interval: 8%;
64 |
65 | // Components
66 | $border-width: 1px;
67 | $border-color: $gray-300;
68 |
69 | $border-radius: .25rem;
70 | $border-radius-lg: .3rem;
71 | $border-radius-sm: .2rem;
72 |
73 | $transition-base: all .2s ease-in-out;
74 | $transition-fade: opacity .15s linear;
75 | $transition-collapse: height .35s ease;
76 |
77 | // Fonts
78 | $font-size-base: 1rem;
79 |
80 | $font-weight-light: 300;
81 | $font-weight-normal: 400;
82 | $font-weight-bold: 700;
83 |
84 |
85 |
86 | // Alerts
87 | $alert-padding-y: .5rem;
88 | $alert-padding-x: 1.25rem;
89 | $alert-margin-bottom: 1rem;
90 | $alert-border-radius: $border-radius;
91 | $alert-link-font-weight: $font-weight-bold;
92 | $alert-border-width: $border-width;
93 |
94 | $alert-bg-level: -10;
95 | $alert-border-level: -9;
96 | $alert-color-level: 6;
97 |
98 | // Close
99 | $close-font-size: $font-size-base * 1.5;
100 | $close-font-weight: $font-weight-bold;
101 | $close-color: $black;
102 | $close-text-shadow: 0 1px 0 $white;
103 |
104 |
105 |
106 | // Tabs
107 | $tab-border-radius: .25rem;
108 |
109 | $tab-ul-bg: #3073bb;
110 |
111 | $tab-link-padding: .75rem 1rem .85rem;
112 | $tab-link-colour: $white;
113 |
114 | $tab-link-width-active: 5px;
115 | $tab-link-bg-colour-active: rgba(0, 0, 0, .2);
116 |
117 | $tab-content-padding: 15px 0;
118 | $tab-content-bg: #fefefe;
119 |
120 | $tab-accordion-link-border: 1px solid #ddd;
121 |
122 | $tab-vertical-ul-border: 1px solid #ccc;
123 |
124 |
125 |
126 | // Dropdown
127 | $dropdown-bg: $white;
128 | $dropdown-border-width: 1px;
129 | $dropdown-border-style: solid;
130 | $dropdown-border-colour: $black;
131 | $dropdown-border-opacity: .15;
132 |
133 | $dropdown-item-padding: .5rem .75rem;
134 | $dropdown-item-bg-hover: $gray-700;
135 | $dropdown-item-color: $gray-900;
136 | $dropdown-item-color-hover: $white;
137 |
--------------------------------------------------------------------------------
/src/scss/alert/alert.scss:
--------------------------------------------------------------------------------
1 | joomla-alert {
2 | --jui-alert-min-width: 250px;
3 | --jui-alert-padding: .5rem 1.25rem;
4 | --jui-alert-margin: 0 0 1rem 0;
5 | --jui-alert-border: 1px solid transparent;
6 | --jui-alert-border-radius: .25rem;
7 | --jui-alert-animation-duration: .5s;
8 | --jui-alert-animation-timing-function: ease-in-out;
9 | --jui-alert-button-color-dark: #000;
10 | --jui-alert-button-color-light: #fff;
11 | --jui-alert-success-color: #234423;
12 | --jui-alert-success-background-color: #d9e6d9;
13 | --jui-alert-success-border-color: #cadcca;
14 | --jui-alert-success-link-color: #122212;
15 | --jui-alert-info-color: #0c5460;
16 | --jui-alert-info-background-color: #d1ecf1;
17 | --jui-alert-info-border-color: #bee5eb;
18 | --jui-alert-info-link-color: #062c33;
19 | --jui-alert-warning-color: #7d5a29;
20 | --jui-alert-warning-background-color: #fcefdc;
21 | --jui-alert-warning-border-color: #fbe8cd;
22 | --jui-alert-warning-link-color: #573e1c;
23 | --jui-alert-danger-color: #712b29;
24 | --jui-alert-danger-background-color: #f7dddc;
25 | --jui-alert-danger-border-color: #f4cfce;
26 | --jui-alert-danger-link-color: #4c1d1b;
27 |
28 | display: block;
29 | min-width: var(--jui-alert-min-width, 250px);
30 | padding: var(--jui-alert-padding, .5rem 1.25rem);
31 | margin: var(--jui-alert-margin, 0 0 1rem 0);
32 | border: var(--jui-alert-border, 1px solid transparent);
33 | border-radius: var(--jui-alert-border-radius, .25rem);
34 | animation-duration: var(--jui-alert-animation-duration, .5s);
35 | animation-timing-function: var(--jui-alert-animation-timing-function, ease-in-out);
36 | }
37 |
38 | joomla-alert .joomla-alert--close {
39 | position: relative;
40 | top: -.5rem;
41 | right: -1.25rem;
42 | float: right;
43 | padding: .2rem 1rem;
44 | font-size: 1.5rem;
45 | font-weight: 700;
46 | line-height: 1;
47 | color: var(--jui-alert-button-color-dark, #000);
48 | text-shadow: 0 1px 0 var(--jui-alert-button-color-light, #fff);
49 | background: transparent;
50 | border: 0;
51 | opacity: .5;
52 | }
53 |
54 | joomla-alert .joomla-alert--close:hover,
55 | joomla-alert .joomla-alert--close:focus {
56 | color: var(--jui-alert-button-color-dark, #000);
57 | text-decoration: none;
58 | cursor: pointer;
59 | opacity: .75;
60 | }
61 |
62 | joomla-alert[type=success] {
63 | color: var(--jui-alert-success-color, #234423);
64 | background-color: var(--jui-alert-success-background-color, #d9e6d9);
65 | border-color: var(--jui-alert-success-border-color, #cadcca);
66 | }
67 |
68 | joomla-alert[type=success] hr {
69 | border-top-color: var(--jui-alert-success-border-color, #cadcca);
70 | }
71 |
72 | joomla-alert[type=success] .alert-link {
73 | color: var(--jui-alert-success-link-color, #122212);
74 | }
75 |
76 | joomla-alert[type=info] {
77 | color: var(--jui-alert-info-color, #0c5460);
78 | background-color: var(--jui-alert-info-background-color, #d1ecf1);
79 | border-color: var(--jui-alert-info-border-color, #bee5eb);
80 | }
81 |
82 | joomla-alert[type=info] hr {
83 | border-top-color: var(--jui-alert-info-border-color, #bee5eb);
84 | }
85 |
86 | joomla-alert[type=info] .alert-link {
87 | color: var(--jui-alert-info-link-color, #062c33);
88 | }
89 |
90 | joomla-alert[type=warning] {
91 | color: var(--jui-alert-warning-color, #7d5a29);
92 | background-color: var(--jui-alert-warning-background-color, #fcefdc);
93 | border-color: var(--jui-alert-warning-border-color, #fbe8cd);
94 | }
95 |
96 | joomla-alert[type=warning] hr {
97 | border-top-color: var(--jui-alert-warning-border-color, #fbe8cd);
98 | }
99 |
100 | joomla-alert[type=warning] .alert-link {
101 | color: var(--jui-alert-warning-link-color, #573e1c);
102 | }
103 |
104 | joomla-alert[type=danger] {
105 | color: var(--jui-alert-danger-color, #712b29);
106 | background-color: var(--jui-alert-danger-background-color, #f7dddc);
107 | border-color: var(--jui-alert-danger-border-color, #f4cfce);
108 | }
109 |
110 | joomla-alert[type=danger] hr {
111 | border-top-color: var(--jui-alert-danger-border-color, #f4cfce);
112 | }
113 |
114 | joomla-alert[type=danger] .alert-link {
115 | color: var(--jui-alert-danger-link-color, #4c1d1b);
116 | }
117 |
118 | // RTL overrides
119 | html[dir=rtl] joomla-alert {
120 | .joomla-alert--close,
121 | .joomla-alert-button--close {
122 | right: auto;
123 | left: -1.25rem;
124 | float: left;
125 | }
126 | }
127 |
128 | @keyframes joomla-alert-fade-in {
129 | 0% { opacity: 0; }
130 | }
131 |
132 | @keyframes joomla-alert-fade-out {
133 | 0% { opacity: 1; }
134 | 100% { opacity: 0; }
135 | }
136 |
137 | @media (prefers-reduced-motion: reduce) {
138 | joomla-alert {
139 | animation-duration: 1ms !important;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/scss/collapse/collapse.scss:
--------------------------------------------------------------------------------
1 | // Collapse
2 |
3 | //
4 | // Variables
5 | //
6 |
7 | @import "../variables";
8 |
9 |
10 | //
11 | // Base styles
12 | //
13 |
14 | joomla-collapse {
15 |
16 | &[state="closed"] {
17 | display: none;
18 | }
19 |
20 | &[state="open"] {
21 | display: block;
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/scss/dropdown/dropdown.scss:
--------------------------------------------------------------------------------
1 | // Dropdown
2 |
3 | //
4 | // Variables
5 | //
6 |
7 | @import "../variables";
8 |
9 |
10 | //
11 | // Base styles
12 | //
13 |
14 |
15 | joomla-dropdown {
16 | position: absolute;
17 | top: 30px;
18 | left: 0;
19 | z-index: 1000;
20 | box-sizing: border-box;
21 | display: none;
22 | min-width: 10rem;
23 | margin-top: .5rem;
24 | font-size: 1rem;
25 | text-align: left;
26 | list-style: none;
27 | background-color: $dropdown-bg;
28 | background-clip: padding-box;
29 | border: $dropdown-border-width $dropdown-border-style rgba($dropdown-border-colour, $dropdown-border-opacity);
30 |
31 | &[expanded] {
32 | display: block;
33 | }
34 |
35 | .dropdown-item {
36 | display: block;
37 | padding: $dropdown-item-padding;
38 | clear: both;
39 | font-weight: $font-weight-normal;
40 | color: $dropdown-item-color;
41 | text-align: inherit;
42 | white-space: nowrap;
43 | background-color: transparent;
44 | border: 0;
45 |
46 | &:hover,
47 | &:focus {
48 | color: $dropdown-item-color-hover;
49 | text-decoration: none;
50 | cursor: pointer;
51 | background: $dropdown-item-bg-hover;
52 | }
53 | }
54 |
55 | }
56 |
57 | .joomla-dropdown-container {
58 | position: relative;
59 | display: inline-flex;
60 | vertical-align: middle;
61 | }
62 |
63 | // RTL overrides
64 | html[dir=rtl] joomla-dropdown {
65 | right: 0;
66 | left: auto;
67 | }
68 |
--------------------------------------------------------------------------------
/src/scss/modal/modal.scss:
--------------------------------------------------------------------------------
1 | // Modal
2 |
3 | //
4 | // Variables
5 | //
6 |
7 | @import "../variables";
8 |
9 |
10 | //
11 | // Base styles
12 | //
13 |
14 | joomla-modal {
15 | position: fixed;
16 | top: 0;
17 | right: 0;
18 | bottom: 0;
19 | left: 0;
20 | z-index: 1050;
21 | box-sizing: inherit;
22 | display: none;
23 | max-width: 500px;
24 | margin: 10px auto;
25 | overflow: hidden;
26 | border-radius: 5px;
27 | outline: 0;
28 |
29 | &.jviewport-width10 {
30 | width: 10vw;
31 | margin-left: -5vw;
32 | }
33 | &.jviewport-width20 {
34 | width: 20vw;
35 | margin-left: -10vw;
36 | }
37 | &.jviewport-width30 {
38 | width: 30vw;
39 | margin-left: -15vw;
40 | }
41 | &.jviewport-width40 {
42 | width: 40vw;
43 | margin-left: -20vw;
44 | }
45 | &.jviewport-width50 {
46 | width: 50vw;
47 | margin-left: -25vw;
48 | }
49 | &.jviewport-width60 {
50 | width: 60vw;
51 | margin-left: -30vw;
52 | }
53 | &.jviewport-width70 {
54 | width: 70vw;
55 | margin-left: -35vw;
56 | }
57 | &.jviewport-width80 {
58 | width: 80vw;
59 | margin-left: -40vw;
60 | }
61 | &.jviewport-width90 {
62 | width: 90vw;
63 | margin-left: -45vw;
64 | }
65 | &.jviewport-width100 {
66 | width: 100vw;
67 | margin-left: -50vw;
68 | }
69 |
70 | &.show {
71 | display: block;
72 | }
73 |
74 | .joomla-modal-dialog {
75 | position: relative;
76 | display: flex;
77 | flex-direction: column;
78 | background-color: #fff;
79 | background-clip: padding-box;
80 | border: 1px solid rgba(0, 0, 0, .2);
81 | border-radius: .3rem;
82 | outline: 0;
83 |
84 | &.fade {
85 | opacity: 0;
86 | transition: opacity .15s linear;
87 | }
88 |
89 | &.fade.show {
90 | opacity: 1;
91 | }
92 |
93 | header {
94 | display: flex;
95 | align-items: center;
96 | justify-content: space-between;
97 | padding: 15px;
98 | border-bottom: 1px solid #e9ecef;
99 |
100 | button {
101 | float: right;
102 | padding: 0;
103 | font-size: 1.5rem;
104 | font-weight: 700;
105 | line-height: 1;
106 | color: #000;
107 | text-shadow: 0 1px 0 #fff;
108 | cursor: pointer;
109 | background: 0 0;
110 | border: 0;
111 | opacity: .5;
112 | appearance: none;
113 | }
114 |
115 | h5 {
116 | margin-bottom: 0;
117 | font-size: 1.25rem;
118 | line-height: 1.5;
119 | }
120 | }
121 |
122 | section {
123 | position: relative;
124 | flex: 1 1 auto;
125 | padding: 15px;
126 |
127 | &.jviewport-height10 {
128 | height: 10vh;
129 | }
130 | &.jviewport-height20 {
131 | height: 20vh;
132 | }
133 | &.jviewport-height30 {
134 | height: 30vh;
135 | }
136 | &.jviewport-height40 {
137 | height: 40vh;
138 | }
139 | &.jviewport-height50 {
140 | height: 50vh;
141 | }
142 | &.jviewport-height60 {
143 | height: 60vh;
144 | }
145 | &.jviewport-height70 {
146 | height: 70vh;
147 | }
148 | &.jviewport-height80 {
149 | height: 80vh;
150 | }
151 | &.jviewport-height90 {
152 | height: 90vh;
153 | }
154 | &.jviewport-height100 {
155 | height: 100vh;
156 | }
157 | }
158 |
159 | section[class^="jviewport-height"],
160 | section[class*="jviewport-height"] {
161 | max-height: none;
162 | }
163 |
164 | footer {
165 | display: flex;
166 | align-items: center;
167 | justify-content: flex-end;
168 | padding: 15px;
169 | border-top: 1px solid #e9ecef;
170 |
171 | .btn {
172 | margin-left: 10px;
173 | }
174 | }
175 | }
176 | }
177 |
178 | .modal-backdrop.show {
179 | opacity: .5;
180 | }
181 | .modal-backdrop {
182 | position: fixed;
183 | top: 0;
184 | right: 0;
185 | bottom: 0;
186 | left: 0;
187 | z-index: 1040;
188 | background-color: #000;
189 | }
190 |
--------------------------------------------------------------------------------
/src/scss/tab/tab.scss:
--------------------------------------------------------------------------------
1 | // Tabs
2 |
3 | //
4 | // Variables
5 | //
6 |
7 | @import "../variables";
8 |
9 |
10 | //
11 | // Base styles
12 | //
13 |
14 | joomla-tab {
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | joomla-tab[view=tabs] > div[role=tablist] {
20 | display: flex;
21 | padding: 0;
22 | margin: 0;
23 | overflow-x: auto;
24 | overflow-y: hidden;
25 | white-space: nowrap;
26 | list-style: outside none none;
27 | background-color: #f5f5f5;
28 | border-color: #ccc #ccc currentcolor;
29 | border-style: solid solid none;
30 | border-width: 1px 1px 0;
31 | border-radius: .25rem .25rem 0 0;
32 | border-image: none;
33 | box-shadow: 0 1px #fff inset, 0 2px 3px -3px rgba(0, 0, 0, .15), 0 -4px 0 rgba(0, 0, 0, .05) inset, 0 0 3px rgba(0, 0, 0, .04);
34 | }
35 |
36 | joomla-tab[view=accordion] > div[role=tablist] {
37 | display: none;
38 | }
39 |
40 | joomla-tab button[role=tab] {
41 | position: relative;
42 | display: block;
43 | padding: .75em 1em;
44 | color: #0d1321;
45 | text-decoration: none;
46 | background-color: transparent;
47 | border: unset;
48 | box-shadow: 1px 0 0 rgba(0, 0, 0, .05);
49 | appearance: none;
50 | }
51 |
52 | joomla-tab button[role=tab][aria-expanded="true"],
53 | joomla-tab button[role=tab][aria-selected="true"] {
54 | background-color: rgba(0, 0, 0, .03);
55 | background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, .05) 100%);
56 | border-right: 0 none;
57 | border-left: 0 none;
58 | border-top-left-radius: 0;
59 | border-top-right-radius: 0;
60 | box-shadow: 2px 0 1px -1px rgba(0, 0, 0, .08) inset, -2px 0 1px -1px rgba(0, 0, 0, .08) inset, 0 1px 0 rgba(0, 0, 0, .02) inset;
61 | }
62 |
63 | joomla-tab button[aria-expanded="true"]::after,
64 | joomla-tab button[aria-selected="true"]::after
65 | {
66 | position: absolute;
67 | right: 0;
68 | bottom: -1px;
69 | left: 0;
70 | height: 5px;
71 | content: "";
72 | background-color: #006898;
73 | opacity: .8;
74 | }
75 |
76 | joomla-tab > joomla-tab-element {
77 | position: relative;
78 | display: none;
79 | padding: 15px;
80 | background-color: #fefefe;
81 | border: 1px solid #ccc;
82 | border-radius: 0 0 .25rem .25rem;
83 | box-shadow: 0 0 3px rgba(0, 0, 0, .04);
84 | }
85 |
86 | joomla-tab > joomla-tab-element[active] {
87 | display: block;
88 | }
89 |
90 | joomla-tab[orientation=vertical] {
91 | flex-direction: row;
92 | align-items: flex-start;
93 | }
94 |
95 | joomla-tab[orientation=vertical] > div[role=tablist] {
96 | flex-direction: column;
97 | min-width: 30%;
98 | height: auto;
99 | overflow: hidden;
100 | border: 1px solid #ccc;
101 | border-radius: .25rem;
102 | box-shadow: none;
103 | }
104 |
105 | joomla-tab[orientation=vertical] > div[role=tablist] button:last-of-type {
106 | border-bottom: 0;
107 | }
108 |
109 | joomla-tab[orientation=vertical] > div[role=tablist] button {
110 | position: relative;
111 | display: block;
112 | padding: .75em 1em;
113 | color: #0d1321;
114 | text-decoration: none;
115 | background-color: transparent;
116 | border-bottom: 1px solid #ddd;
117 | box-shadow: none;
118 | appearance: none;
119 | }
120 |
121 | joomla-tab[orientation=vertical] > div[role=tablist] button[aria-expanded="true"] {
122 | background-color: #fff;
123 | background-image: none;
124 | border-right: 0 none;
125 | border-left: 0 none;
126 | box-shadow: none;
127 | }
128 |
129 | joomla-tab[orientation=vertical] > div[role=tablist] button[aria-expanded="true"]::after {
130 | top: 0;
131 | bottom: 0;
132 | left: -1px;
133 | width: 5px;
134 | height: auto;
135 | }
136 |
137 | joomla-tab[orientation=vertical] > joomla-tab-element {
138 | padding: 15px;
139 | border: 0 none;
140 | box-shadow: none;
141 | }
142 |
143 | joomla-tab[view=accordion] {
144 | flex-direction: column;
145 | white-space: normal;
146 | border-radius: .25rem;
147 | box-shadow: 0 1px #fff inset, 0 0 3px rgba(0, 0, 0, .04);
148 |
149 | > button {
150 | position: relative;
151 | display: block;
152 | padding: .75em 1em;
153 | color: #0d1321;
154 | text-decoration: none;
155 | background-color: #f5f5f5;
156 | border: unset;
157 | box-shadow: 1px 0 0 rgba(0, 0, 0, .05);
158 | appearance: none;
159 |
160 | &[aria-expanded=true],
161 | &:focus {
162 | background-color: rgba(0, 0, 0, .03);
163 | background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, .05) 100%);
164 | }
165 | }
166 | }
167 |
168 | joomla-tab[view=accordion] joomla-tab-element {
169 | display: none;
170 | padding: 15px;
171 | }
172 |
173 | joomla-tab[view=accordion] joomla-tab-element[active] {
174 | display: block;
175 | border-bottom: 1px solid #ddd;
176 | }
177 |
178 | joomla-tab[view=accordion] [active] {
179 | background-color: #fff;
180 | }
181 |
182 | joomla-tab[view=accordion] button {
183 | appearance: none;
184 | border-bottom: 1px solid #ddd;
185 | }
186 |
187 | joomla-tab[view=accordion] button[aria-expanded="true"]::after {
188 | top: 0;
189 | left: 0;
190 | width: 5px;
191 | height: 100%;
192 | }
193 |
--------------------------------------------------------------------------------
/src/scss/tip/tip.scss:
--------------------------------------------------------------------------------
1 | // Tooltip
2 |
3 | //
4 | // Variables
5 | //
6 |
7 | @import "../variables";
8 |
9 |
10 | //
11 | // Base styles
12 | //
13 |
14 | joomla-tip {
15 | position: relative;
16 | display: inline-block;
17 |
18 | button {
19 | width: 1.6rem;
20 | height: 1.6rem;
21 | font-family: serif;
22 | font-size: 1.4rem;
23 | font-weight: bold;
24 | line-height: 1.4rem;
25 | color: #fff;
26 | background: #1c3d5c;
27 | border: 0;
28 | border-radius: 50%;
29 | }
30 |
31 | .toggletip-bubble {
32 | position: absolute;
33 | z-index: 1040;
34 | display: inline-block;
35 | width: 14rem;
36 | padding: .5rem .8rem;
37 | font-size: .9rem;
38 | line-height: 1.2rem;
39 | color: #fff;
40 | background: #222;
41 | border-radius: .25rem;
42 | box-shadow: 0 0 5px rgba(0,0,0,.4);
43 | transition: all ease-in;
44 | animation-duration: .3s;
45 |
46 | &::after {
47 | position: absolute;
48 | top: .6rem;
49 | right: 100%;
50 | width: 0;
51 | height: 0;
52 | content: "";
53 | border-style: solid;
54 | }
55 |
56 | &.top {
57 | bottom: 100%;
58 | left: 50%;
59 | margin-bottom: .6rem;
60 | transform: translate(-50%, 0);
61 | animation-name: toggletip-fadeInTop;
62 |
63 | &::after {
64 | top: 100%;
65 | bottom: auto;
66 | left: 50%;
67 | border-color: #222 transparent transparent;
68 | border-width: 6px 6px 0;
69 | transform: translateX(-50%);
70 | }
71 |
72 | }
73 |
74 | &.left {
75 | top: 50%;
76 | right: 100%;
77 | margin-right: .6rem;
78 | transform: translate(0, -50%);
79 | animation-name: toggletip-fadeInLeft;
80 |
81 | &::after {
82 | top: 50%;
83 | bottom: auto;
84 | left: 100%;
85 | border-color: transparent transparent transparent #222;
86 | border-width: 6px 0 6px 6px;
87 | transform: translateY(-50%);
88 | }
89 |
90 | }
91 |
92 | &.right {
93 | top: 50%;
94 | left: 100%;
95 | margin-left: .6rem;
96 | transform: translate(0, -50%);
97 | animation-name: toggletip-fadeInRight;
98 |
99 | &::after {
100 | top: 50%;
101 | right: 100%;
102 | bottom: auto;
103 | border-color: transparent #222 transparent transparent;
104 | border-width: 6px 6px 6px 0;
105 | transform: translateY(-50%);
106 | }
107 |
108 | }
109 |
110 | &.bottom {
111 | top: 100%;
112 | left: 50%;
113 | margin-top: .6rem;
114 | transform: translate(-50%, 0);
115 | animation-name: toggletip-fadeInBottom;
116 |
117 | &::after {
118 | top: -6px;
119 | left: 50%;
120 | border-color: transparent transparent #222;
121 | border-width: 0 6px 6px;
122 | transform: translateX(-50%);
123 | }
124 |
125 | }
126 |
127 | }
128 |
129 | }
130 |
131 | @keyframes toggletip-fadeInRight {
132 | from {
133 | opacity: 0;
134 | transform: translate(-10px, -50%);
135 | }
136 | to {
137 | opacity: 1;
138 | transform: translate(0, -50%);
139 | }
140 | }
141 | @keyframes toggletip-fadeInLeft {
142 | from {
143 | opacity: 0;
144 | transform: translate(10px, -50%);
145 | }
146 | to {
147 | opacity: 1;
148 | transform: translate(0, -50%);
149 | }
150 | }
151 | @keyframes toggletip-fadeInTop {
152 | from {
153 | opacity: 0;
154 | transform: translate(-50%, 10px);
155 | }
156 | to {
157 | opacity: 1;
158 | transform: translate(-50%, 0);
159 | }
160 | }
161 | @keyframes toggletip-fadeInBottom {
162 | from {
163 | opacity: 0;
164 | transform: translate(-50%, -10px);
165 | }
166 | to {
167 | opacity: 1;
168 | transform: translate(-50%, 0);
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/tests/alert/alert.html:
--------------------------------------------------------------------------------
1 |
2 | Has some text
3 |
4 |
--------------------------------------------------------------------------------
/tests/alert/test.js:
--------------------------------------------------------------------------------
1 | describe('', function(){
2 | beforeEach(function() {
3 | fixture.setBase('tests/alert');
4 | fixture.load('alert.html', true);
5 | });
6 | afterEach(function() {
7 | fixture.cleanup();
8 | });
9 |
10 | it('Custom Element script is loaded', function(){
11 | expect(customElements.get('joomla-alert')).toBeTrue;
12 | });
13 |
14 | it('Has type info', function() {
15 | const type = fixture.el.firstElementChild.getAttribute('type')
16 |
17 | expect(type).toEqual('info');
18 | });
19 |
20 | it('Respects type attribute change, any unsupported value', function() {
21 | fixture.el.firstElementChild.setAttribute('type', 'unknown');
22 | const type = fixture.el.firstElementChild.getAttribute('type');
23 |
24 | expect(type).toEqual('info');
25 | });
26 |
27 | it('Respects type attribute change, warning', function() {
28 | fixture.el.firstElementChild.setAttribute('type', 'warning');
29 | const type = fixture.el.firstElementChild.getAttribute('type');
30 |
31 | expect(type).toEqual('warning');
32 | });
33 |
34 | it('Respects type attribute change, danger', function() {
35 | fixture.el.firstElementChild.setAttribute('type', 'danger');
36 | const type = fixture.el.firstElementChild.getAttribute('type');
37 |
38 | expect(type).toEqual('danger');
39 | });
40 |
41 | it('Respects type attribute change, success', function() {
42 | fixture.el.firstElementChild.setAttribute('type', 'success');
43 | const type = fixture.el.firstElementChild.getAttribute('type');
44 |
45 | expect(type).toEqual('success');
46 | });
47 |
48 | it('Has the right text', function() {
49 | const type = fixture.el.firstElementChild.querySelector('#text').innerText;
50 |
51 | expect(type).toEqual('Has some text');
52 | });
53 | });
54 |
55 | describe('', function(){
56 | beforeEach(function() {
57 | fixture.setBase('tests/alert');
58 | fixture.load('alert.html', true);
59 | });
60 | afterEach(function() {
61 | fixture.cleanup();
62 | });
63 |
64 | it('Has close button', function() {
65 | const type = fixture.el.firstElementChild.hasAttribute('dismiss');
66 |
67 | expect(type).toBeTrue;
68 | });
69 |
70 | it('Respects button attribute change, false', function() {
71 | const el = fixture.el.firstElementChild;
72 | el.setAttribute('dismiss', 'false');
73 | const type = el.getAttribute('dismiss');
74 | const closeBtn = el.querySelectorAll('.joomla-alert--close');
75 | const close = el.querySelectorAll('button');
76 | expect(type).toBe('false');
77 | expect(closeBtn.length === 0).toBeTrue;
78 | expect(close.length === 0).toBeTrue();
79 | });
80 |
81 | it('Respects button attribute change, true', function() {
82 | const el = fixture.el.firstElementChild;
83 | el.setAttribute('dismiss', 'true');
84 | const type = el.getAttribute('dismiss');
85 | const closeBtn = el.querySelectorAll('.joomla-alert--close');
86 | const close = el.querySelectorAll('button');
87 | expect(type).toBe('true');
88 | expect(closeBtn.length === 1).toBeTrue;
89 | expect(close.length === 1).toBeTrue();
90 | });
91 |
92 | it('Method close removes the alert', function() {
93 | const el = fixture.el.firstElementChild;
94 | el.close();
95 | const type = el.querySelector('joomla-alert');
96 |
97 | expect(type).toBe(null);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/tests/tab/tab.html:
--------------------------------------------------------------------------------
1 |
2 | First tab content
3 |
4 | Second tab content
5 |
6 | Third tab content
7 |
8 |
--------------------------------------------------------------------------------