UNPKG

ngui-tools

Version:

A GUI typesetting display engine and cross platform GUI application development framework based on NodeJS/OpenGL

890 lines (778 loc) 21.2 kB
/* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright © 2015-2016, xuewen.chu * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of xuewen.chu nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ import './util'; import { EventNoticer, NativeNotification } from './event'; var ngui = process.binding('ngui'); var View = ngui.View; var Text = ngui.Text; var extend = util.extend; function reset_data_bind_attrs(responder /* ViewController or View */ ) { if (responder.__bind) { responder.__bind.attrs = []; } } function unregister_data_bind(self, id, responder) { delete responder.__bind; self.onViewData.off(id); } function register_data_bind(self, responder/* ViewController or View */) { var bind = responder.__bind; if (bind) return bind; var id = util.id; responder.__bind = bind = { id: id, attrs: [ /* attr_vx [names,type,value] */ ], replace: null, /*{ mode: 0, // mode: 0 inner text | 1 full replace | 2 ctr.view vx: null, // view xml data relation_views: [], }*/ }; responder.onRemoveView.once(function() { unregister_data_bind(self, id, responder); }); self.onViewData.on2(function(responder) { var bind = responder.__bind; var replace = bind.replace; if (replace) { // full replace var {vx,relation_views} = replace; var exec = vx.v; if (replace.mode == 1) { // replace all relation views var { next, parent } = relation_views[relation_views.length - 1]; // First delete all relationviews, avoid ID repeats relation_views.forEach(v=>v.remove()); load_view_from_bind_data(self, parent, next, vx); } else if (replace.mode == 2) { // replace ctr.view var {vx:t,v} = exec(self.m_vdata, self); // 这里返回的数据必须都为元数据 util.assert(t === 0); let [tag] = v; let view = new tag(); self.view = view; load_view(self, view, value); register_data_bind_2(self, view, 2, vx, [view]); // } else { // replace inner text string responder.innerText = exec(self.m_vdata, self); } } else { // attributes bind bind.attrs.forEach(function(attr_vx) { var [names,type,exec] = attr_vx; var len = names.length - 1; var name = names[len]; var target = responder; for (var i = 0; i < len; i++) { target = target[names[i]]; } target[name] = exec(self.m_vdata, self); }); } }, responder, id); return bind; } function register_data_bind_2(self, responder, mode, vx, relation_views) { register_data_bind(self, responder).replace = { mode: mode, vx: vx, relation_views: relation_views }; } function set_attrbute(self, responder, attr_vx) { // [names,type,value] var [names,type,value,multiple] = attr_vx; var target = responder; var len = names.length - 1; var name = names[len]; for (var i = 0; i < len; i++) { target = target[names[i]]; } if (type === 0) { target[name] = value; } else if (type == 3) { // data bind target[name] = value(self.m_vdata, self); if (multiple) { // multiple bind register_data_bind(self, responder).attrs.push(attr_vx); } } else { throw new TypeError('Unknown view xml attribute type'); } } /** * 载入视图并绑定数据响应 */ function load_view_from_bind_data(self, parent, next, raw_vx) { // The data returned from the data binding must all be metadata var {v:exec} = raw_vx; var vx = exec(self.m_vdata, self); // 返回的数据必须都为元数据,不能返回{vx:3}的数据 var out = { responder: null, mode: 0, relation_views: [] }; load_view_from_bind_data_2(self, parent, next, vx); register_data_bind_2(self, out.responder, out.mode, raw_vx, out.relation_views.reverse()); } function load_view_from_bind_data_2(self, parent, next, vx, out) { var {vx:t, v} = vx; var r_view = null; switch(t) { case 0: var [tag] = v; var responder = new tag(); if (responder instanceof ViewController) { // ctr load_subctr(self, responder, vx, parent, next); r_view = responder.view; } else { // view if (next) { next.before(responder); } else { responder.appendTo(parent); } load_view(self, responder, vx); r_view = responder; } out.mode = 1; out.responder = responder; break; case 1: throw new TypeError('Unimplemented <prefix:suffix />'); break; case 3: throw new TypeError('Data binding must return metadata'); break; case 2: // string default: if (t != 2 && Array.isArray(vx)) { for (var item of vx.slice().reverse()) { next = load_view_from_bind_data_2(self, parent, next, item, out); } r_view = next; } else { // string append text r_view = parent.appendText(vx); if (r_view) { if (next) { next.before(r_view); // Right position } out.mode = 1; out.responder = r_view; } else { // replace inner text if (out.mode !== 0) { throw new TypeError('Data bound list item pattern mismatch'); } out.responder = r_view = parent; } } break; } out.relation_views.push(r_view); return r_view; } // empty view xml <View /> export const EMPTY_VIEW_XML = {vx:0,v:[View,[],[]]}; // Is empty view xml export function isEmptyViewXml(vx) { return vx === EMPTY_VIEW_XML; } /** * @func isViewXml(vx[,type]) * @arg vx {Object} * @arg [type] {class} * @ret {[`bool`]} */ export function isViewXml(vx, type) { // {vx:0,v:[tag,[attrs],[child],vdata]} if (vx && vx.vx === 0) { var v = vx.v; if (v) { var [tag] = v; if (tag) { if ( type ) { return util.equalsClass(type, tag); } else { return true; } } } } return false; } function load_subctr(self, subctr, vx, parent, next) { var [,attrs,childs,vdata] = vx.v; if (vdata) { set_attrbute(self, subctr, vdata); } if (childs.length) { subctr.loadView(...childs); } else { subctr.loadView(EMPTY_VIEW_XML); } var view = subctr.view; if (!view) { subctr.view = view = new View(); } if (next) { next.before(view); } else { view.appendTo(parent); } reset_data_bind_attrs(subctr); for (var attr of attrs) { set_attrbute(self, subctr, attr); } } function load_child_view(self, parent, vx) { var {vx:t,v} = vx; // View xml data format info // {vx:0,v:[tag,[attrs],[child],vdata]} <tag /> // {vx:1,v:[prefix,suffix,[attrs],[child],vdata]} <prefix:suffix /> // {vx:2,v:"string"} string // {vx:3,v:exec,m:1} %{xx} or %%{xx} // {vx:4,v:value} ${xx} switch (t) { case 0: // <tag /> var [tag] = v; var obj = new tag(); if (obj instanceof ViewController) { load_subctr(self, obj, vx, parent, null); } else { // view obj.appendTo(parent); load_view(self, obj, vx); } break; case 1: // <prefix:suffix /> throw new TypeError('Unimplemented <prefix:suffix />'); break; case 2: // string parent.appendText(v); break; case 3: if (vx.m) { // %%{xx} multiple load_view_from_bind_data(self, parent, null, vx); } else { // %{xx} let vx = v(self.m_vdata, self); // exec load_child_view(self, parent, vx); } break; default: // Unknown, check type if (Array.isArray(vx)) { for (var item of vx) { load_child_view(self, parent, item); } } else { parent.appendText(vx); } break; } } function load_view0(self, vx) { var view; var {vx:t,v} = vx; switch(t) { case 0: // <tag /> var [tag] = v; view = new tag(); // 这里必需为视图,不可以为控制器 self.view = view; load_view(self, view, vx); break; case 1: // <prefix:suffix /> throw new TypeError('Unimplemented <prefix:suffix />'); break; case 2: // string view = new Text(); view.value = v; self.view = view; break; case 3: // %%{xx} or %{xx} var vx2 = v(self.m_vdata, self); // 这里返回的数据必须都为元数据 // util.assert(isViewXml(vx2)); var [tag] = vx2.v; view = new tag(); // tag必须为View self.view = view; load_view(self, view, vx2); if (vx.m) { // multiple bind register_data_bind_2(self, view, 2, vx, [view]); } break; default: // Unknown if (Array.isArray(vx)) { view = load_view0(self, vx[0]); } else { view = new Text(); view.value = vx; self.view = view; } break; } return view; } function load_view(self, view, vx) { var [,attrs,childs] = vx.v; for (var ch of childs) { load_child_view(self, view, ch); } reset_data_bind_attrs(view); for (var attr of attrs) { set_attrbute(self, view, attr); } } // -------------------- no ctr ---------------------- function set_attrbute_no_ctr(obj, attr_vx) { // [names,type,value] var [names,type,value] = attr_vx; var target = obj; var len = names.length - 1; var name = names[len]; for (var i = 0; i < len; i++) { target = target[names[i]]; } if (type === 0) { target[name] = value; } else { // data bind throw new TypeError('Bad argument. Cannot bind data'); } } function load_subctr_no_ctr(subctr, vx, parent) { var [,attrs,childs,vdata] = vx.v; if (vdata) { set_attrbute_no_ctr(subctr, vdata); } if (childs.length) { subctr.loadView(...childs); } else { subctr.loadView(EMPTY_VIEW_XML); } var view = subctr.view; if (!view) { subctr.view = view = new View(); } if (parent) { view.appendTo(parent); } for (var attr of attrs) { set_attrbute_no_ctr(subctr, attr); } } function load_child_view_no_ctr(parent, vx) { var {vx:t,v} = vx; switch (t) { case 0: // <tag /> let [tag] = v; let obj = new tag(); if (obj instanceof ViewController) { load_subctr_no_ctr(obj, vx, parent); } else { // view obj.appendTo(parent); load_view_no_ctr(obj, vx); } break; case 1: // <prefix:suffix /> throw new TypeError('Unimplemented <prefix:suffix />'); break; case 2: // string parent.appendText(v); break; case 3: // %%{xx} or %{xx} throw new TypeError('Bad argument. Cannot bind data'); break; default: if (Array.isArray(vx)) { for (var item of vx) { load_child_view_no_ctr(parent, item); } } else { parent.appendText(vx); } break; } } function load_view_no_ctr(view, vx) { var [,attrs,childs] = vx.v; for (var ch of childs) { load_child_view_no_ctr(view, ch); } for (var attr of attrs) { set_attrbute_no_ctr(view, attr); } } /** * @func New(vx[,parent[,...args]]) view or view controller with vx data * @func New(vx[,...args]) * @arg vx {Object} * @arg [parent] {View} * @arg [...args] * @ret {View|ViewController} */ export function New(vx, parent, ...args) { if (isViewXml(vx)) { if ( parent ) { if ( !(parent instanceof View) ) { args.unshift(parent); parent = null; } } var [tag] = vx.v; var rv = new tag(...args); var ctr = null; if ( parent ) { ctr = parent.ctr; if ( !ctr ) { if ( parent.top ) { ctr = parent.top.ctr; } } } if ( rv instanceof View ) { if ( parent ) { rv.appendTo(parent); } if ( ctr ) { load_view(ctr, rv, vx); } else { load_view_no_ctr(rv, vx); } return rv; } else if ( rv instanceof ViewController ) { if ( ctr ) { load_subctr(ctr, rv, vx, parent, null); } else { load_subctr_no_ctr(rv, vx, parent); } return rv; } } throw new TypeError('Bad argument. invalid view xml data'); } /** * @class NativeViewController * * @get parent {ViewController} * * @get,set view {View} * * @get,set id {uint} * * @func find(id) * @arg id {String} * @ret {View|ViewController) * * @func remove() * * @end */ /** * @class ViewController * @bases NativeViewController */ export class ViewController extends ngui.NativeViewController { m_vdata = null; // 视图数据 m_mapping = null; /** * @event onViewData */ event onViewData; /** * @event onLoadView */ event onLoadView; /** * @event onRemoveView */ event onRemoveView; /* events mapping */ event onBack; event onClick; event onTouchStart; event onTouchMove; event onTouchEnd; event onTouchCancel; event onKeyDown; event onKeyPress; event onKeyUp; event onKeyEnter; event onFocus; event onBlur; event onHighlighted; event onFocusMove; event onScroll; event onActionKeyframe; event onActionLoop; event onWaitBuffer; // player event onReady; event onStartPlay; event onError; event onSourceEof; event onPause; event onResume; event onStop; event onSeek; /** * @get vdata {Object} */ get vdata() { return this.m_vdata } /** * @set set vdata {Object} */ set vdata(value) { if (typeof value == 'object') { extend(this.m_vdata, value); this.triggerViewDataChange(); } } /** * @get view {View} */ get view() { return super.view } /** * @set view {View} */ set view(value) { var __bind = this.__bind; super.view = value; if (__bind) { // 如果之前有绑定动态数据,设置新的视图后会被清理 if (__bind !== this.__bind) { var parent = this.parent; if ( parent ) { // 重新设置原bind var cur_bind = register_data_bind(parent, this); __bind.id = cur_bind.id; extend(cur_bind, __bind); } } } } /** * @constructor() */ constructor() { super(); this.m_vdata = {}; } /** * @func loadView(vx) * @arg vx {Object} */ loadView(vx) { load_view0(this, vx); // reset event mapping var mapping = this.m_mapping; if (mapping) { // unbind mapping for ( var name in mapping ) { add_event_mapping(this, name, self['__on' + name]); } } this.triggerLoadView(); } /** * @get action {Action} */ get action() { // get action object return this.view.action; } /** * @set action {Action} */ set action(value) { // set action this.view.action = value; } /** * @func transition(style[,delay[,cb]][,cb]) * @arg style {Object} * @arg [delay] {uint} ms * @arg [cb] {Funcion} */ transition(style, delay, cb) { // transition animate this.view.transition(style, delay, cb); } /** * @func show() */ show() { this.view.show(); } /** * @func show() */ hide() { this.view.hide(); } /** * @get class {Object} */ get 'class'() { return this.view.class; } /** * @set class {String} */ set 'class'(value) { this.view.class = value; } /** * @func addClass(name) * @arg name {String} */ addClass(name) { this.view.addClass(name); } /** * @func removeClass(name) * @arg name {String} */ removeClass(name) { this.view.removeClass(name); } /** * @func toggleClass(name) * @arg name {String} */ toggleClass(name) { this.view.toggleClass(name); } /** * @get style {Object} */ get style() { return this.view.style; } /** * @get style {Object} */ set style(value) { this.view.style = value; } /** * @get visible {bool} */ get visible() { return this.view.visible; } /** * @get visible {bool} */ set visible(value) { this.view.visible = value; } /** * @get receive {bool} */ get receive() { return this.view.receive; } /** * @get receive {bool} */ set receive(value) { this.view.receive = value; } /** * @overwrite native call */ triggerRemoveView(view) { util.assert(this.view === view); // unbind mapping var mapping = this.m_mapping; if (mapping) { for ( var name in mapping ) { var id = mapping[name]; if (id > 0) { var noticer = view['__on' + name]; //.off(name, id); if ( noticer ) { noticer.off(id); } } mapping[name] = 0; } } this.trigger('RemoveView', view); } } const event_mapping_table = { Keydown: 1, KeyPress: 1, KeyUp: 1, KeyEnter: 1, Back: 1, Click: 1, TouchStart: 1, TouchMove: 1, TouchEnd: 1, TouchCancel: 1, Focus: 1, Blur: 1, Highlighted: 1, FocusMove: 1, Scroll: 1, ActionKeyframe: 1, ActionLoop: 1, WaitBuffer: 1, Ready: 1, StartPlay: 1, Error: 1, SourceEof: 1, Pause: 1, Resume: 1, Stop: 1, Seek: 1, }; function add_event_mapping(self, noticer, name) { if ( name in event_mapping_table ) { // mapping event var mapping = self.m_mapping; if (!mapping) { self.m_mapping = mapping = {}; } var view = self.view; util.assert(view, 'View not found'); var name2 = 'on' + name; if ( name2 in view ) { // if ( !noticer ) { self['__on' + name] = noticer = new EventNoticer(name, this); } var trigger = self['trigger' + name]; mapping[name] = view[name2].on((evt) => { var origin_noticer = evt.m_noticer; trigger.call(self, evt, 1); evt.m_noticer = origin_noticer; }); return noticer; } else { mapping[name] = -1; } } } /** * @class ViewControllerNotification */ class ViewControllerNotification extends NativeNotification { /** * @overwrite */ $getNoticer(name) { var noticer = this['__on' + name]; if ( ! noticer ) { // bind native event noticer = add_event_mapping(this, noticer, name); if ( noticer ) { return noticer; } var trigger = this['trigger' + name]; // bind native event if ( trigger ) { // bind native util.addNativeEventListener(this, name, (evt, is_event) => { // native event return trigger.call(this, evt, is_event); }, -1); } else { // bind native util.addNativeEventListener(this, name, (evt, is_event) => { // native event return is_event ? noticer.triggerWithEvent(evt) : noticer.trigger(evt); }, -1); } this['__on' + name] = noticer = new EventNoticer(name, this); } else { var mapping = this.m_mapping; if ( mapping && mapping[name] ) { return noticer; } add_event_mapping(this, noticer, name); } return noticer; } /** * @overwrite */ $addDefaultListener(name, func) { if ( typeof func == 'string' ) { var ctr = this, func2; while (ctr) { func2 = ctr[func]; // find func if ( typeof func2 == 'function' ) { return this.$getNoticer(name).on(func2, ctr, 0); // default id 0 } ctr = ctr.parent; } throw util.err(`Cannot find a function named "${func}"`); } else { return this.$getNoticer(name).on(func, 0); // default id 0 } } } util.extendClass(ViewController, ViewControllerNotification);