Commit 5ee0dcbb authored by akiraohgaki's avatar akiraohgaki

Merge branch 'develop'

parents 74b72942 5631226d
Pipeline #302 passed with stage
in 5 minutes and 31 seconds
build_*/
node_modules/
bin/
dist/
......@@ -7,5 +7,6 @@
"width": 1024,
"height": 768
}
}
},
"updateCheckAfter": 86400000, "//": "milliseconds"
}
{
"bin": "bin/ocs-manager",
"bin": "ocs-manager",
"port": 0
}
.icon-ocs-store {
background-image: url(app-icons/ocs-store.svg);
}
@media screen {
.icon-ocs-store {
background-image: url(app-icons/ocs-store.svg);
}
.icon-chevron-left {
background-image: url(material-design-icons/ic_chevron_left_black_48px.svg);
}
.icon-chevron-right {
background-image: url(material-design-icons/ic_chevron_right_black_48px.svg);
}
.icon-refresh {
background-image: url(material-design-icons/ic_refresh_48px.svg);
}
.icon-close {
background-image: url(material-design-icons/ic_close_black_48px.svg);
}
.icon-folder {
background-image: url(material-design-icons/ic_folder_black_48px.svg);
}
.icon-home {
background-image: url(material-design-icons/ic_home_black_48px.svg);
}
.icon-info {
background-image: url(material-design-icons/ic_info_outline_white_48px.svg);
}
.icon-menu {
background-image: url(material-design-icons/ic_menu_black_48px.svg);
}
.icon-loading {
background-image: url(misc/loading.svg);
}
}
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
\ No newline at end of file
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
\ No newline at end of file
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
\ No newline at end of file
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
\ No newline at end of file
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
\ No newline at end of file
<svg fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11 17h2v-6h-2v6zm1-15C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zM11 9h2V7h-2v2z"/>
</svg>
\ No newline at end of file
<svg fill="#000000" height="48" viewBox="0 0 24 24" width="48" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M35.3 12.7C32.41 9.8 28.42 8 24 8 15.16 8 8.02 15.16 8.02 24S15.16 40 24 40c7.45 0 13.69-5.1 15.46-12H35.3c-1.65 4.66-6.07 8-11.3 8-6.63 0-12-5.37-12-12s5.37-12 12-12c3.31 0 6.28 1.38 8.45 3.55L26 22h14V8l-4.7 4.7z"/></svg>
\ No newline at end of file
Material design icons
https://github.com/google/material-design-icons/
Apache License Version 2.0
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="64px" height="64px" viewBox="0 0 128 128" xml:space="preserve"><g><path d="M78.75 16.18V1.56a64.1 64.1 0 0 1 47.7 47.7H111.8a49.98 49.98 0 0 0-33.07-33.08zM16.43 49.25H1.8a64.1 64.1 0 0 1 47.7-47.7V16.2a49.98 49.98 0 0 0-33.07 33.07zm33.07 62.32v14.62A64.1 64.1 0 0 1 1.8 78.5h14.63a49.98 49.98 0 0 0 33.07 33.07zm62.32-33.07h14.62a64.1 64.1 0 0 1-47.7 47.7v-14.63a49.98 49.98 0 0 0 33.08-33.07z" fill="#000000" fill-opacity="1"/><animateTransform attributeName="transform" type="rotate" from="0 64 64" to="-90 64 64" dur="800ms" repeatCount="indefinite"></animateTransform></g></svg>
\ No newline at end of file
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline'; connect-src *">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="format-detection" content="telephone=no, email=no, address=no">
<title></title>
<link href="./images/images.css" rel="stylesheet">
<link href="./styles/reset.css" rel="stylesheet">
<link href="./styles/style.css" rel="stylesheet">
<link href="styles/reset.css" rel="stylesheet">
<link href="styles/color.css" rel="stylesheet">
<link href="styles/material-icons.css" rel="stylesheet">
<link href="styles/global.css" rel="stylesheet">
</head>
<body>
<div data-component="Root"></div>
<app-root></app-root>
<script>
require('import-export'); // Polyfill to use ES6 modules syntax in nodejs
require('./scripts/renderers/window-renderer.js');
require = require('esm')(module);
module.exports = require('./scripts/renderers/browser-window.js');
</script>
</body>
......
......@@ -2,13 +2,14 @@
* Chirit
*
* @author Akira Ohgaki <akiraohgaki@gmail.com>
* @copyright Akira Ohgaki
* @copyright 2018, Akira Ohgaki
* @license https://opensource.org/licenses/BSD-2-Clause
* @link https://github.com/akiraohgaki/chirit
*/
import Component from './Component.js';
import StateManager from './StateManager.js';
import Handler from './Handler.js';
import WebStorage from './WebStorage.js';
import Utility from './Utility.js';
......@@ -22,6 +23,10 @@ export default class Chirit {
return StateManager;
}
static get Handler() {
return Handler;
}
static get WebStorage() {
return WebStorage;
}
......
......@@ -2,53 +2,208 @@
* Chirit
*
* @author Akira Ohgaki <akiraohgaki@gmail.com>
* @copyright Akira Ohgaki
* @copyright 2018, Akira Ohgaki
* @license https://opensource.org/licenses/BSD-2-Clause
* @link https://github.com/akiraohgaki/chirit
*/
export default class Component {
export default class Component extends HTMLElement {
// Subclass should use init() instead of constructor()
constructor(element, state) {
// "element" should be Element object or selector string
if (typeof element === 'string') {
element = document.querySelector(element);
}
static define(name, options) {
window.customElements.define(name, this, options);
}
constructor() {
super();
this.element = element || document.createElement('div');
this.innerHTML = this.element.innerHTML;
this.state = state;
this._state = {};
this._shadowRoot = null;
this._template = null;
this._updateCount = 0;
this.initShadow();
this.initTemplate();
this.init();
this._build();
this.complete();
}
_build() {
const html = this.html();
const style = this.style();
this.element.innerHTML = style ? `<style>${style}</style>${html}` : html;
this.script();
set state(state) {
this.setState(state);
}
get state() {
return this.getState();
}
get contentRoot() {
return this._shadowRoot || this.shadowRoot || this;
}
initShadow() {
this.enableShadow();
}
initTemplate() {
this.importTemplate(document.createElement('template'));
}
setState(state) {
if (typeof state !== 'object' || state === null) {
throw new TypeError(`"${state}" is not an object`);
}
const oldState = Object.assign({}, this._state);
this._state = state;
const newState = Object.assign({}, this._state);
this._stateChangedCallback(oldState, newState);
}
getState() {
return this._state;
}
setContent(content) {
// "content" should be Node object or string
if (typeof content === 'string') {
const template = document.createElement('template');
template.innerHTML = content;
content = template.content;
}
const oldContent = this.exportTemplate().content;
this._template.content.textContent = null;
this._template.content.appendChild(content);
const newContent = this.exportTemplate().content;
this.contentRoot.textContent = null;
this.contentRoot.appendChild(newContent.cloneNode(true));
this._contentChangedCallback(oldContent, newContent);
}
enableShadow(options = {}) {
this._shadowRoot = this.attachShadow(Object.assign(
{mode: 'open'},
options
));
}
importTemplate(template) {
if (!(template instanceof HTMLTemplateElement)) {
throw new TypeError(`"${template}" is not a HTMLTemplateElement`);
}
this._template = template.cloneNode(true);
}
exportTemplate() {
return this._template.cloneNode(true);
}
dispatch(type, data = {}) {
this.dispatchEvent(new CustomEvent(type, {
detail: data,
bubbles: true,
composed: true
}));
}
update(state) {
this.state = state;
this._build();
if (state !== undefined) {
this.setState(state);
}
else {
this._update();
}
}
_update() {
let content = this.render();
if (typeof content !== 'string' && !content) {
content = this.exportTemplate().content;
}
this.setContent(content);
this._updatedCallback();
}
// Abstract methods
init() {}
complete() {}
render() {}
// Lifecycle methods
static get componentObservedAttributes() {
return [];
}
componentAttributeChangedCallback() {}
componentConnectedCallback() {}
componentDisconnectedCallback() {}
componentAdoptedCallback() {}
componentStateChangedCallback() {}
componentContentChangedCallback() {}
componentUpdatedCallback() {}
html() {
return '';
// Lifecycle methods in parent class
static get observedAttributes() {
return this.componentObservedAttributes;
}
style() {
return '';
attributeChangedCallback(attributeName, oldValue, newValue, namespace) {
this.componentAttributeChangedCallback(attributeName, oldValue, newValue, namespace);
if (this._updateCount && oldValue !== newValue) {
this._update();
}
}
connectedCallback() {
this.componentConnectedCallback();
if (!this._updateCount) {
this._update();
}
}
script() {}
disconnectedCallback() {
this.componentDisconnectedCallback();
}
adoptedCallback(oldDocument, newDocument) {
this.componentAdoptedCallback(oldDocument, newDocument);
if (!this._updateCount) {
this._update();
}
}
// Additional lifecycle methods
_stateChangedCallback(oldState, newState) {
this.componentStateChangedCallback(oldState, newState);
//if (this._updateCount && JSON.stringify(oldState) !== JSON.stringify(newState)) {
// this.update();
//}
if (this._updateCount) {
this._update();
}
}
_contentChangedCallback(oldContent, newContent) {
this.componentContentChangedCallback(oldContent, newContent);
}
_updatedCallback() {
this._updateCount++;
this.componentUpdatedCallback();
}
}
/**
* Chirit
*
* @author Akira Ohgaki <akiraohgaki@gmail.com>
* @copyright 2018, Akira Ohgaki
* @license https://opensource.org/licenses/BSD-2-Clause
* @link https://github.com/akiraohgaki/chirit
*/
export default class Handler {
constructor(handler) {
// handler:
// (data = {}, type = '') => {
// return {};
// }
this._initialHandler = handler;
this._defaultHandler = null;
this._typeHandlersCollection = new Map(); // [[type, [handler]]]
this.resetDefault();
}
resetDefault() {
this.setDefault(this._initialHandler);
return this;
}
setDefault(handler) {
this._checkTypeOfHandler(handler);
this._defaultHandler = handler;
this.defaultChangedCallback(handler);
return this;
}
add(type, handler) {
this._checkTypeOfHandler(handler);
this.beforeAddCallback(type, handler);
const typeHandlers = this._typeHandlersCollection.get(type) || new Set();
if (!typeHandlers.has(handler)) {
typeHandlers.add(handler);
this._typeHandlersCollection.set(type, typeHandlers);
this.afterAddCallback(type, handler);
}
return this;
}
remove(type, handler) {
this._checkTypeOfHandler(handler);
this.beforeRemoveCallback(type, handler);
if (this._typeHandlersCollection.has(type)) {
const typeHandlers = this._typeHandlersCollection.get(type);
if (typeHandlers.has(handler)) {
typeHandlers.delete(handler);
if (typeHandlers.size) {
this._typeHandlersCollection.set(type, typeHandlers);
}
else {
this._typeHandlersCollection.delete(type);
}
this.afterRemoveCallback(type, handler);
}
}
return this;
}
has(type) {
return this._typeHandlersCollection.has(type);
}
async invoke(data = {}, type = '') {
// This function will wrap and call registered handlers with Promise and Promise.all().
// And all return values of the same type of handlers will be combined in object finally.
// If any handler returned false, will not combine values and return null.
const promises = [];
promises.push(new Promise((resolve) => {
resolve(this._defaultHandler(data, type));
}));
if (type && this._typeHandlersCollection.has(type)) {
const typeHandlers = this._typeHandlersCollection.get(type);
for (const handler of typeHandlers) {
promises.push(new Promise((resolve) => {
resolve(handler(data, type));
}));
}
}
const values = await Promise.all(promises);
if (values.includes(false)) {
return null;
}
const combinedData = {};
for (const value of values) {
Object.assign(combinedData, value);
}
return combinedData;
}
defaultChangedCallback() {}
beforeAddCallback() {}
afterAddCallback() {}
beforeRemoveCallback() {}
afterRemoveCallback() {}
_checkTypeOfHandler(handler) {
if (typeof handler !== 'function') {
throw new TypeError(`"${handler}" is not a function`);
}
}
}
......@@ -4,6 +4,6 @@
A front-end library.
Copyright: Akira Ohgaki
Copyright: 2018, Akira Ohgaki
License: BSD-2-Clause
......@@ -2,122 +2,140 @@
* Chirit
*
* @author Akira Ohgaki <akiraohgaki@gmail.com>
* @copyright Akira Ohgaki
* @copyright 2018, Akira Ohgaki
* @license https://opensource.org/licenses/BSD-2-Clause
* @link https://github.com/akiraohgaki/chirit
*/
import Handler from './Handler.js';
export default class StateManager {
constructor(eventTarget) {
// "eventTarget" should be Element object or selector string
if (typeof eventTarget === 'string') {
eventTarget = document.querySelector(eventTarget);
constructor(target) {
// "target" should be Element object or selector string
if (typeof target === 'string') {
target = document.querySelector(target);
}
this._eventTarget = eventTarget || document;
this._eventListener = (event) => {
event.preventDefault();
event.stopPropagation();
this.dispatch(event.type, event.detail);
};
this._target = target || document;
this._state = new Map();
this._eventListener = this._eventListener.bind(this);
this._states = new Map();
this._actions = new Map();
this._views = new Map();
this._eventHandler = null;
this._actionHandler = null;
this._stateHandler = null;
this._viewHandler = null;
this._initHandlers();
}
getStates() {
return this._states;
get target() {
return this._target;
}
getState(type) {
return this._states.get(type);
get state() {
return this._state;
}
registerAction(type, action, options) {
const actions = this._actions.has(type) ? this._actions.get(type) : new Map();
if (!actions.size) {
this._states.set(type, {});
this._eventTarget.addEventListener(type, this._eventListener, false);
}
actions.set(action, options);
this._actions.set(type, actions);
get eventHandler() {
return this._eventHandler;
}
unregisterAction(type, action) {
if (this._actions.has(type)) {
const actions = this._actions.get(type);
if (actions.has(action)) {
actions.delete(action);
if (actions.size) {
this._actions.set(type, actions);
}
else {
this._actions.delete(type);
this._states.delete(type);
this._eventTarget.removeEventListener(type, this._eventListener, false);
}
}
}
get actionHandler() {
return this._actionHandler;
}
registerView(type, view, options) {
const views = this._views.has(type) ? this._views.get(type) : new Map();
views.set(view, options);
this._views.set(type, views);
get stateHandler() {
return this._stateHandler;
}
unregisterView(type, view) {
if (this._views.has(type)) {
const views = this._views.get(type);
if (views.has(view)) {
views.delete(view);
if (views.size) {
this._views.set(type, views);
}
else {
this._views.delete(type);
}
get viewHandler() {
return this._viewHandler;
}
dispatch(type, data = {}) {
this._target.dispatchEvent(new CustomEvent(type, {detail: data}));
}
_eventListener(event) {
event.preventDefault();
event.stopPropagation();
this._invokeHandlers(event.detail, event.type);
}
_initHandlers() {
this._eventHandler = new Handler((data) => {
return data;
});
this._actionHandler = new Handler(() => {
return {};
});
this._stateHandler = new Handler((data, type) => {
this._state.set(type, data);
return data;
});